`std :: list<> :: sort()` - 为什么突然切换到自上而下策略?

时间:2016-11-16 01:06:07

标签: c++ algorithm list sorting mergesort

我记得自开始以来,最常用的实现std::list<>::sort()的方法是在bottom-up fashion中实现的经典合并排序算法(另请参阅What makes the gcc std::list sort implementation so fast?)。

我记得有人恰如其分地将这个策略称为&#34;洋葱链&#34;方法

至少与GCC实现C ++标准库的方式相同(例如,参见here)。这就是旧版Dimkumware的标准库MSVC版本中的STL,以及所有版本的MSVC一直到VS2013的情况。

然而,随VS2015提供的标准库突然不再遵循此排序策略。 VS2015附带的库使用自上而下 Merge Sort的相当简单的递归实现。这让我感到很奇怪,因为自上而下的方法需要访问列表的中点才能将其分成两半。由于std::list<>不支持随机访问,因此找到该中间点的唯一方法是逐字遍历列表的一半。此外,在最开始时,有必要知道列表中的元素总数(在C ++ 11之前不一定是O(1)操作。)

尽管如此,VS2015中的std::list<>::sort()确实如此。以下是该实现的摘录,它定位中点并执行递归调用

...
iterator _Mid = _STD next(_First, _Size / 2);
_First = _Sort(_First, _Mid, _Pred, _Size / 2);
_Mid = _Sort(_Mid, _Last, _Pred, _Size - _Size / 2);
...

正如您所看到的,他们只是无动于衷地使用std::next遍历列表的前半部分并到达_Mid迭代器。

我想知道这种转换背后的原因是什么?我所看到的是在每个递归级别对std::next的重复调用看似明显的低效率。天真的逻辑说这是更慢。如果他们愿意支付这种价格,他们可能希望获得回报。那他们得到了什么?我没有立即将此算法视为具有更好的缓存行为(与原始自下而上的方法相比)。我不会立即看到它在预先排序的序列上表现得更好。

当然,由于C ++ 11 std::list<>基本上需要存储其元素数,这使得上面的效率略高,因为我们总是提前知道元素数。但这仍然不足以证明每个递归级别的顺序扫描是正确的。

(不可否认,我还没有试图相互竞争实施。也许有一些惊喜。)

2 个答案:

答案 0 :(得分:21)

第一次更新 - VS2015引入了非默认可构造和有状态分配器,这在使用本地列表时会出现问题,就像之前的自下而上方法一样。我能够通过使用节点指针而不是列表(见下文)来处理这个问题,以实现自下而上的方法。

第二次更新 - 虽然从列表到迭代器的切换是解决分配器和异常处理问题的一种方法,但是从上到下切换为自下而上是不必要的,自下而上可以使用迭代器实现。我使用迭代器创建了一个自下而上的合并排序,并且在VS2015自顶向下方法中使用了基本相同的合并/拼接逻辑。这是答案的结尾。

在@ sbi的评论中,他向作者提出了自上而下的批评,Stephan T. Lavavej,为什么要做出改变。斯蒂芬的反应是“避免内存分配和默认构建分配器&#34;”。新的自顶向下方法比旧的自底向上方法慢,但它只使用迭代器(递归存储在堆栈中),不使用任何本地列表,并避免与非默认可构造或有状态分配器相关的问题。合并操作使用splice()和迭代器来移动&#34;移动&#34;列表中的节点,提供异常安全性(假设splice()不能失败)。 @ T.C。的回答详细介绍了这一点。 第二次更新 - 但是,自下而上的方法也可以基于迭代器和基本相同的合并逻辑(本答案底部的示例代码)。一旦确定了合并逻辑,我就不确定为什么基于迭代器和基于拼接的合并的自下而上方法没有被调查。

至于性能,如果有足够的内存,将列表移动到数组或向量,排序,然后将已排序的数组或向量移回列表通常会更快。

我能够根据@IgorTandetnik的演示重现这个问题(旧的编译无法编译,新的编译工作):

#include <iostream>
#include <list>
#include <memory>

template <typename T>
class MyAlloc : public std::allocator<T> {
public:
    MyAlloc(T) {}  // suppress default constructor

    template <typename U>
    MyAlloc(const MyAlloc<U>& other) : std::allocator<T>(other) {}

    template< class U > struct rebind { typedef MyAlloc<U> other; };
};

int main()
{
    std::list<int, MyAlloc<int>> l(MyAlloc<int>(0));
    l.push_back(3);
    l.push_back(0);
    l.push_back(2);
    l.push_back(1);
    l.sort();
    return 0;
}

我在2016年7月发现了这一变化,并于2016年8月1日通过电子邮件向P.J. Plauger发送了关于此更改的电子邮件。他回复的片段:

  

有趣的是,我们的更改日志并未反映此更改。那   可能意味着它是#34;建议&#34;由我们的一个大客户和   得到了我的代码审查。我现在所知道的是变化来了   在2015年秋天左右。当我查看代码时,第一个   令我印象深刻的是这条线:

    iterator _Mid = _STD next(_First, _Size / 2);
     

当然,对于大型列表来说,非常可以花费很长时间。

     

代码看起来比1995年初写的更优雅(!),   但肯定有更糟糕的时间复杂性。该版本已建模   在Stepanov,Lee和Musser在原STL中接近之后。   他们在选择算法时很少被发现错误。

     

我现在正在恢复原始代码的最新已知良好版本。

我不知道P.J.Plauger对原始代码的回复是否涉及新的分配器问题,或者Microsoft是否或如何与Dinkumware交互。

为了比较自顶向下和自底向上的方法,我创建了一个包含400万个元素的链表,每个元素由一个64位无符号整数组成,假设我最终会得到几乎顺序排列的节点的双向链表(即使它们将被动态分配),用随机数填充它们,然后对它们进行排序。节点不移动,只改变链接,但现在遍历列表以随机顺序访问节点。然后我用另一组随机数填充那些随机排序的节点并再次对它们进行排序。我将2015年自上而下的方法与之前自下而上的方法进行了比较,修改后的方法与2015年的其他更改相匹配(sort()现在使用谓词比较函数调用sort(),而不是使用两个单独的函数)。这些是结果。 update - 我添加了一个基于节点指针的版本,并且还注明了从列表创建矢量,排序矢量,复制回来的时间。

sequential nodes: 2015 version 1.6 seconds, prior version 1.5  seconds
random nodes:     2015 version 4.0 seconds, prior version 2.8  seconds
random nodes:                  node pointer based version 2.6  seconds
random nodes:    create vector from list, sort, copy back 1.25 seconds

对于顺序节点,先前版本仅稍微快一点,但对于随机节点,先前版本快30%,节点指针版本快35%,并从列表创建向量,对向量进行排序,然后复制的速度提高了69%。

下面是std :: list :: sort()的第一个替换代码我用来比较先前的自下而上和小数组(_BinList [])方法与VS2015的自上而下方法我希望比较到公平,所以我修改了一份&lt;列表&gt;。

    void sort()
        {   // order sequence, using operator<
        sort(less<>());
        }

    template<class _Pr2>
        void sort(_Pr2 _Pred)
        {   // order sequence, using _Pred
        if (2 > this->_Mysize())
            return;
        const size_t _MAXBINS = 25;
        _Myt _Templist, _Binlist[_MAXBINS];
        while (!empty())
            {
            // _Templist = next element
            _Templist._Splice_same(_Templist.begin(), *this, begin(),
                ++begin(), 1);
            // merge with array of ever larger bins
            size_t _Bin;
            for (_Bin = 0; _Bin < _MAXBINS && !_Binlist[_Bin].empty();
                ++_Bin)
                _Templist.merge(_Binlist[_Bin], _Pred);
            // don't go past end of array
            if (_Bin == _MAXBINS)
                _Bin--;
            // update bin with merged list, empty _Templist
            _Binlist[_Bin].swap(_Templist);
            }
            // merge bins back into caller's list
            for (size_t _Bin = 0; _Bin < _MAXBINS; _Bin++)
                if(!_Binlist[_Bin].empty())
                    this->merge(_Binlist[_Bin], _Pred);
        }

我做了一些小改动。原始代码跟踪名为_Maxbin的变量中的实际最大bin,但最终合并中的开销足够小,以至于我删除了与_Maxbin关联的代码。在数组构建期间,原始代码的内部循环合并到_Binlist []元素中,然后交换到_Templist,这似乎毫无意义。我将内部循环更改为仅合并到_Templist中,只有在找到空的_Binlist []元素时才进行交换。

下面是一个基于节点指针的替换std :: list :: sort()我用于另一个比较。这消除了分配相关的问题。如果可能发生比较异常,则必须将数组和临时列表(pNode)中的所有节点追加回原始列表,或者可能将比较异常视为小于比较。

    void sort()
        {   // order sequence, using operator<
        sort(less<>());
        }

    template<class _Pr2>
        void sort(_Pr2 _Pred)
        {   // order sequence, using _Pred
        const size_t _NUMBINS = 25;
        _Nodeptr aList[_NUMBINS];           // array of lists
        _Nodeptr pNode;
        _Nodeptr pNext;
        _Nodeptr pPrev;
        if (this->size() < 2)               // return if nothing to do
            return;
        this->_Myhead()->_Prev->_Next = 0;  // set last node ->_Next = 0
        pNode = this->_Myhead()->_Next;     // set ptr to start of list
        size_t i;
        for (i = 0; i < _NUMBINS; i++)      // zero array
            aList[i] = 0;
        while (pNode != 0)                  // merge nodes into array
            {
            pNext = pNode->_Next;
            pNode->_Next = 0;
            for (i = 0; (i < _NUMBINS) && (aList[i] != 0); i++)
                {
                pNode = _MergeN(_Pred, aList[i], pNode);
                aList[i] = 0;
                }
            if (i == _NUMBINS)
                i--;
            aList[i] = pNode;
            pNode = pNext;
            }
        pNode = 0;                          // merge array into one list
        for (i = 0; i < _NUMBINS; i++)
            pNode = _MergeN(_Pred, aList[i], pNode);
        this->_Myhead()->_Next = pNode;     // update sentinel node links
        pPrev = this->_Myhead();            //  and _Prev pointers
        while (pNode)
            {
            pNode->_Prev = pPrev;
            pPrev = pNode;
            pNode = pNode->_Next;
            }
        pPrev->_Next = this->_Myhead();
        this->_Myhead()->_Prev = pPrev;
        }

    template<class _Pr2>
        _Nodeptr _MergeN(_Pr2 &_Pred, _Nodeptr pSrc1, _Nodeptr pSrc2)
        {
        _Nodeptr pDst = 0;          // destination head ptr
        _Nodeptr *ppDst = &pDst;    // ptr to head or prev->_Next
        if (pSrc1 == 0)
            return pSrc2;
        if (pSrc2 == 0)
            return pSrc1;
        while (1)
            {
            if (_DEBUG_LT_PRED(_Pred, pSrc2->_Myval, pSrc1->_Myval))
                {
                *ppDst = pSrc2;
                pSrc2 = *(ppDst = &pSrc2->_Next);
                if (pSrc2 == 0)
                    {
                    *ppDst = pSrc1;
                    break;
                    }
                }
            else
                {
                *ppDst = pSrc1;
                pSrc1 = *(ppDst = &pSrc1->_Next);
                if (pSrc1 == 0)
                    {
                    *ppDst = pSrc2;
                    break;
                    }
                }
            }
        return pDst;
        }

作为新VS2015 std :: list :: sort()的替代方案,您可以使用此独立版本。

template <typename T>
void listsort(std::list <T> &dll)
{
    const size_t NUMLISTS = 32;
    std::list <T> al[NUMLISTS]; // array of lists
    std::list <T> tl;           // temp list
    while (!dll.empty()){
        // t1 = next element from dll
        tl.splice(tl.begin(), dll, dll.begin(), std::next(dll.begin()));
        // merge element into array
        size_t i;
        for (i = 0; i < NUMLISTS && !al[i].empty(); i++){
            tl.merge(al[i], std::less<T>());
        }
        if(i == NUMLISTS)       // don't go past end of array
            i -= 1;
        al[i].swap(tl);         // update array list, empty tl
    }
    // merge array back into original list
    for(size_t i = 0; i < NUMLISTS; i++)
        dll.merge(al[i], std::less<T>());
}

或使用类似的gcc算法。

更新#2:我已经使用一小部分迭代器编写了一个自下而上的合并排序,并且基本上是通过来自VS2015 std :: list :: sort的splice函数进行基于迭代器的合并,这应该消除VS2015的std :: list :: sort解决了分配器和异常问题。示例代码如下。在Merge()中对splice()的调用有点棘手,最后一个迭代器在实际调用splice之前后递增,这是因为在std :: list中实现迭代器后增量的方式,补偿了拼接。数组操作的自然顺序避免了合并/拼接操作中迭代器的任何损坏。数组中的每个迭代器都指向已排序子列表的开头。每个已排序子列表的结尾将是数组中下一个先前非空条目中的排序子列表的开头,或者如果在数组的开头,则是变量中的。

// 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++)           // "clear" 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;
        }
    }
}

VS2015&gt; std :: list :: sort()的替换代码(添加内部函数_Merge):

    template<class _Pr2>
        iterator _Merge(_Pr2& _Pred, iterator li, iterator ri, iterator ei)
        {
        iterator ni;
        _DEBUG_LT_PRED(_Pred, *ri, *li) ? ni = ri : ni = li;
        while(1)
            {
            if(_DEBUG_LT_PRED(_Pred, *ri, *li))
                {
                splice(li, *this, ri++);
                if(ri == ei)
                    return ni;
                }
            else
                {
                if(++li == ri)
                    return ni;
                }
            }
        }

    void sort()
        {   // order sequence, using operator<
        sort(less<>());
        }

    template<class _Pr2>
        void sort(_Pr2 _Pred)
        {
        if (size() < 2)                 // if size < 2 nothing to do
            return;
        const size_t _ASZ = 32;         // array size
        iterator ai[_ASZ];              // array of iterators
        iterator li;                    // left  iterator
        iterator ri;                    // right iterator
        iterator ei = end();            // end iterator
        size_t i;
        for(i = 0; i < _ASZ; i++)       // "clear array"
            ai[i] = ei;
        // merge nodes into array
        for(ei = begin(); ei != end();)
            {
            ri = ei++;
            for (i = 0; (i < _ASZ) && ai[i] != end(); i++)
                {
                ri = _Merge(_Pred, ai[i], ri, ei);
                ai[i] = end();
                }
            if(i == _ASZ)
                i--;
            ai[i] = ri;
            }
        // merge array into single list
        ei = 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(_Pred, li, ri, ei);
            }
        }

VS2019的替换代码std :: list :: sort()(添加内部函数_Merge,并使用VS模板命名约定):

private:
    template <class _Pr2>
    iterator _Merge(_Pr2 _Pred, iterator _First, iterator _Mid, iterator _Last){
        iterator _Newfirst = _First;
        for (bool _Initial_loop = true;;
            _Initial_loop       = false) { // [_First, _Mid) and [_Mid, _Last) are sorted and non-empty
            if (_DEBUG_LT_PRED(_Pred, *_Mid, *_First)) { // consume _Mid
                if (_Initial_loop) {
                    _Newfirst = _Mid; // update return value
                }
                splice(_First, *this, _Mid++);
                if (_Mid == _Last) {
                    return _Newfirst; // exhausted [_Mid, _Last); done
                }
            }
            else { // consume _First
                ++_First;
                if (_First == _Mid) {
                    return _Newfirst; // exhausted [_First, _Mid); done
                }
            }
        }
    }

    template <class _Pr2>
    void _Sort(iterator _First, iterator _Last, _Pr2 _Pred,
        size_type _Size) { // order [_First, _Last), using _Pred, return new first
                           // _Size must be distance from _First to _Last
        if (_Size < 2) {
            return;        // nothing to do
        }
        const size_t _ASZ = 32;         // array size
        iterator _Ai[_ASZ];             // array of iterators to runs
        iterator _Mi;                   // middle   iterator
        iterator _Li;                   // last     iterator
        size_t _I;                      // index to _Ai
        for (_I = 0; _I < _ASZ; _I++)   // "empty" array
            _Ai[_I] = _Last;            //   _Ai[] == _Last => empty entry
        // merge nodes into array
        for (_Li = _First; _Li != _Last;) {
            _Mi = _Li++;
            for (_I = 0; (_I < _ASZ) && _Ai[_I] != _Last; _I++) {
                _Mi = _Merge(_Pass_fn(_Pred), _Ai[_I], _Mi, _Li);
                _Ai[_I] = _Last;
            }
            if (_I == _ASZ)
                _I--;
            _Ai[_I] = _Mi;
        }
        // merge array runs into single run
        for (_I = 0; _I < _ASZ && _Ai[_I] == _Last; _I++);
        _Mi = _Ai[_I++];
        while (1) {
            for (; _I < _ASZ && _Ai[_I] == _Last; _I++);
            if (_I == _ASZ)
                break;
            _Mi = _Merge(_Pass_fn(_Pred), _Ai[_I++], _Mi, _Last);
        }
    }

答案 1 :(得分:9)

@sbi asked Stephan T. Lavavej,MSVC的标准库维护者,who responded

  

我这样做是为了避免内存分配和默认构建   分配器。

对此,我将添加“免费基本异常安全”。

详细说明:VS2015之前的实施存在一些缺陷:

  • _Myt _Templist, _Binlist[_MAXBINS];创建了一堆中间list s _Myt只是list当前实例化的typedef;对此来说,拼写错误就是这样, list)在排序过程中保存节点,但这些list是默认构造的,这会导致许多问题:
    1. 如果使用的分配器不是默认可构造的(并且不要求分配器是默认构造的),那么这根本就不会编译,因为list的默认构造函数将尝试默认构造其分配器。 / LI>
    2. 如果使用的分配器是有状态的,则默认构造的分配器可能不等于this->get_allocator(),这意味着后来的splicemerge在技术上是未定义的行为,可能会在调试版本中中断。 (“技术上”,因为节点都在最后合并回来,所以如果函数成功完成,你实际上不会使用错误的分配器解除分配。)
    3. Dinkumware的list使用动态分配的前哨节点,这意味着上述节点将执行_MAXBINS + 1动态分配。我怀疑很多人希望sort可能会抛出bad_alloc。如果分配器是有状态的,则可能甚至不能从正确的位置分配这些标记节点(参见#2)。
  • 代码不是异常安全的。特别是,允许​​比较抛出,如果在中间list s中有元素时抛出,则在堆栈展开期间使用list简单地销毁这些元素。如果sort抛出异常,sort的用户不希望对列表进行排序,但他们可能也不希望元素丢失。
    • 这与上面#2的交互非常糟糕,因为现在它不仅仅是技术上未定义的行为:那些中间list的析构函数将解除分配,并使用错误的分配器销毁拼接到它们中的节点。

这些缺陷是否可以修复?大概。通过将get_allocator()传递给list的构造函数来修复#1和#2:

 _Myt _Templist(get_allocator());
 _Myt _Binlist[_MAXBINS] = { _Myt(get_allocator()), _Myt(get_allocator()), 
                             _Myt(get_allocator()),  /* ... repeat _MAXBINS times */ };

可以通过使用try-catch围绕循环来修复异常安全问题,该list将中间*this中的所有节点拼接回list,而不考虑顺序,如果异常是抛出。

修复#3更难,因为这意味着根本不使用list作为节点的持有者,这可能需要大量的重构,但它是可行的。

问题是:是否值得跳过所有这些环节以改善设计性能降低的容器的性能?毕竟,真正关心性能的人可能不会首先使用 JsonArray[0]= { "streetName" : "xyz", "wardName" : "12xz"} JsonArray[1]= { "doorNumber" : "123", "plot" : "90z"} JsonArray[2]= { "city" : "pqr", "district" : "nmc"} JsonArray[3]= { "state" : "hisd", "country" : "kasps"}