【问题标题】:C++ STL algorithm (list sort) OpenMP/multithreaded implementationsC++ STL 算法(列表排序) OpenMP/多线程实现
【发布时间】:2016-02-14 10:06:14
【问题描述】:

我试图加速我的代码的一个内核,其本质归结为一种(我在具有多个内核的 CPU 上运行它)。 我从这篇文章 (STL algorithms and concurrent programming) 中发现,其中一些算法可以加速,例如,使用 OpenMP(见下文)。

我使用 __gnu_parallel::sort 获得了相当不错的加速

例如

__gnu_parallel::sort(std::begin(X), std::end(X), [](X a, X b){ return a.member > b.member;});

事实证明,std::list 是一个更好的数据容器。但这似乎没有用于排序的并行/多线程实现。

上面链接的帖子是 2010 年发布的。我想知道最新的智慧是什么。

【问题讨论】:

  • 如何将线程数作为您拥有的 CPU 数,将列表分成相等的块并让每个线程同步排序?
  • "What is the cur" -- 你是否在写这句话时分心而忘记完成它?
  • @DavidHaim - 在线程全部完成后,您仍然需要再次对结果进行排序,除非当然列表已经排序。直觉上,我觉得这种方法比简单的单线程排序更糟糕,因为您也已经不得不对块进行排序。当然,有些算法比其他算法更好地处理已经排序的数据,但我认为这充其量只是缓存未命中地狱。
  • 排序list 总是有问题,你确定不能用deque 代替吗?看看这里:baptiste-wicht.com/posts/2012/12/… 进行一些比较。
  • 是的,我确信我们可以相应地并行化这个问题,但我只是在寻找一种无需做太多工作的解决方案。事实上,我们可以很容易地划分问题(我知道情况就是这样,因为问题是令人尴尬的并行),如果我们想做这样的事情,可以使用 MPI。

标签: c++ list sorting concurrency stl


【解决方案1】:

在 Microsoft(Visual Studio 2015 之前)的情况下,std::list::sort 使用列表数组,其中 array[i] 是一个空列表或大小为 2 的 i 次幂的列表( 1,2,4,8, ...)。节点从原始列表中一次取出一个并合并到数组中,然后将数组合并以形成单个排序列表。假设比较开销并不过分,这是一个内存绑定过程,并且由于扫描列表以拆分列表的开销,多线程将无济于事,几乎使内存读取操作的数量增加了一倍。这是这种类型的列表排序的示例代码,其限制是比较是

#define NUMLISTS 32                     /* number of lists */
NODE * SortList(NODE *pList)
{
NODE * aList[NUMLISTS];                 /* array of lists */
NODE * pNode;
NODE * pNext;
int i;
    if(pList == NULL)                   /* check for empty list */
        return NULL;
    for(i = 0; i < NUMLISTS; i++)       /* zero array */
        aList[i] = NULL;
    pNode = pList;                      /* merge nodes into array */
    while(pNode != NULL){
        pNext = pNode->next;
        pNode->next = NULL;
        for(i = 0; (i < NUMLISTS) && (aList[i] != NULL); i++){
            pNode = MergeLists(aList[i], pNode);
            aList[i] = NULL;
        }
        if(i == NUMLISTS)
            i--;
        aList[i] = pNode;
        pNode = pNext;
    }
    pNode = NULL;                       /* merge array into one list */
    for(i = 0; i < NUMLISTS; i++)
        pNode = MergeLists(aList[i], pNode);
    return pNode;
}

NODE * MergeLists(NODE *pSrc1, NODE *pSrc2)
{
NODE *pDst = NULL;                      /* destination head ptr */
NODE **ppDst = &pDst;                   /* ptr to head or prev->next */
    if(pSrc1 == NULL)
        return pSrc2;
    if(pSrc2 == NULL)
        return pSrc1;
    while(1){
        if(pSrc2->data < pSrc1->data){  /* if src2 < src1 */
            *ppDst = pSrc2;
            pSrc2 = *(ppDst = &pSrc2->next);
            if(pSrc2 == NULL){
                *ppDst = pSrc1;
                break;
            }
        } else {                        /* src1 <= src2 */
            *ppDst = pSrc1;
            pSrc1 = *(ppDst = &pSrc1->next);
            if(pSrc1 == NULL){
                *ppDst = pSrc2;
                break;
            }
        }
    }
    return pDst;
}

更新 - Visual Studio 2015 及更高版本切换到使用迭代器而不是列表进行合并排序,这消除了诸如没有默认分配器之类的分配问题,并且由于合并是通过 splice() 在同一个列表上完成的,因此它提供了异常安全性(如果用户比较抛出异常,列表将重新排序,但所有节点都在那里,假设 splice 从不抛出异常)。 VS2015 也切换到自上而下的归并排序,但可以使用基于迭代器的自下而上归并排序。我不确定为什么要切换到自上而下,因为对于具有随机分散节点的大型列表(远远超出缓存大小),它会慢 40% 左右。基于迭代器的示例代码。数组中的每个迭代器都指向大小为 2 的 i 次幂的运行的第一个节点,或者它等于 list.end(),以指示空运行。运行的结束将是数组中的第一个非“空”条目或局部变量迭代器(数组中的所有运行都是相邻的运行)。所有合并都涉及相邻的运行。合并函数有 3 个参数,一个指向左运行第一个节点的迭代器,一个指向右运行第一个节点的迭代器,这也是左运行的结束,以及一个指向右运行结束的迭代器 (这可能是以下运行的第一个节点或 list.end()) 的迭代器。

template <typename T>
typename std::list<T>::iterator Merge(std::list<T> &ll,
                    typename std::list<T>::iterator li,
                    typename std::list<T>::iterator ri,
                    typename std::list<T>::iterator ei);

// iterator array size
#define ASZ 32

template <typename T>
void SortList(std::list<T> &ll)
{
    if (ll.size() < 2)                  // return if nothing to do
        return;
    std::list<T>::iterator ai[ASZ];     // array of iterators
    std::list<T>::iterator li;          // left   iterator
    std::list<T>::iterator ri;          // right  iterator
    std::list<T>::iterator ei;          // end    iterator
    size_t i;
    for (i = 0; i < ASZ; i++)           // "empty" array
        ai[i] = ll.end();
    // merge nodes into array
    for (ei = ll.begin(); ei != ll.end();) {
        ri = ei++;
        for (i = 0; (i < ASZ) && ai[i] != ll.end(); i++) {
            ri = Merge(ll, ai[i], ri, ei);
            ai[i] = ll.end();
        }
        if(i == ASZ)
            i--;
        ai[i] = ri;
    }
    // merge array into single list
    ei = ll.end();                              
    for(i = 0; (i < ASZ) && ai[i] == ei; i++);
    ri = ai[i++];
    while(1){
        for( ; (i < ASZ) && ai[i] == ei; i++);
        if (i == ASZ)
            break;
        li = ai[i++];
        ri = Merge(ll, li, ri, ei);
    }
}

template <typename T>
typename std::list<T>::iterator Merge(std::list<T> &ll,
                             typename std::list<T>::iterator li,
                             typename std::list<T>::iterator ri,
                             typename std::list<T>::iterator ei)
{
    std::list<T>::iterator ni;
    (*ri < *li) ? ni = ri : ni = li;
    while(1){
        if(*ri < *li){
            ll.splice(li, ll, ri++);
            if(ri == ei)
                return ni;
        } else {
            if(++li == ri)
                return ni;
        }
    }
}

【讨论】:

  • 解释。我知道由于内存带宽争用,更多的内核不会转化为更多的加速,而且 memcpy 是禁忌。所以看起来 STL 列表排序是串行的,除非用户自己实现。
  • @user2875665 - 问题在于链表本质上是串行的。如果可以改用具有随机访问的容器,则可以拆分数据,以便排序算法的大部分早期部分将发生在每个内核的 L1 / L2 缓存中,从而减少内存带宽开销。假设某种类型的合并排序,一旦排序运行的大小显着超过本地缓存大小并恢复到内存绑定进程、许多处理器上的所有内核通用的组合 L3 缓存和主内存,优势就会消失。
  • 感谢您如此清楚地说明了列表的问题(以及问题太大而无法合并到缓存中)。
【解决方案2】:

从 C++ 17 开始,您可以考虑并行排序算法std::sort(std::execution::par, ...

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-09-18
    • 1970-01-01
    相关资源
    最近更新 更多