Я помню, что с самого начала наиболее популярным подходом к реализации std::list<>::sort()
был классический алгоритм Merge Sort, реализованный в снизу вверх (см. также Что делает реализацию сортировки gcc std:: list так быстро?).
Я помню, как кто-то со всей определенностью ссылался на эту стратегию как на подход "привязки лука".
По крайней мере, так, как это происходит в GCC-реализации стандартной библиотеки С++ (см., например, здесь). И так было в старой версии Dimkumware STL в стандартной библиотеке MSVC, а также во всех версиях MSVC вплоть до VS2013.
Однако стандартная библиотека, поставляемая с VS2015, внезапно перестает следовать этой стратегии сортировки. Библиотека, поставляемая с VS2015, использует довольно простую рекурсивную реализацию слияния Merge Sort сверху вниз. Это кажется мне странным, так как подход сверху вниз требует доступа к середине списка, чтобы разделить его пополам. Так как std::list<>
не поддерживает случайный доступ, единственный способ найти эту середину - буквально перебирать половину списка. Кроме того, в самом начале необходимо знать общее количество элементов в списке (что не обязательно было операцией O (1) до С++ 11).
Тем не менее, std::list<>::sort()
в VS2015 делает именно это. Здесь выдержка из этой реализации, которая находит середину и выполняет рекурсивные вызовы
...
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
iterator.
Что может быть причиной этого переключения, интересно? Все, что я вижу, это кажущаяся очевидная неэффективность повторяющихся вызовов std::next
на каждом уровне рекурсии. Наивная логика говорит, что это медленнее. Если они готовы заплатить такую цену, они, вероятно, ожидают получить что-то взамен. Что они потом получают? Я не сразу вижу, что этот алгоритм имеет лучшее поведение кэша (по сравнению с исходным подходом снизу вверх). Я не сразу вижу, как он лучше себя ведет на предварительно отсортированных последовательностях.
Конечно, поскольку С++ 11 std::list<>
в основном требуется хранить свой счетчик элементов, что делает выше немного более эффективным, так как мы всегда знаем количество элементов заранее. Но этого все еще недостаточно, чтобы оправдать последовательное сканирование на каждом уровне рекурсии.
(По общему признанию, я не пытался участвовать в гонках друг против друга. Возможно, там есть какие-то сюрпризы.)