Подтвердить что ты не робот

Очень быстрая сортировка массивов фиксированной длины с использованием сетей компараторов

У меня есть критический код производительности, который включает в себя сортировку очень короткого массива с фиксированной длиной между примерно 3 и 10 элементами в С++ (параметр изменяется во время компиляции).

Мне пришло в голову, что статическая сеть сортировки, специализирующаяся на каждом возможном размере ввода, возможно, была бы очень эффективным способом сделать это: мы делаем все сравнения, необходимые для определения того, в каком случае мы находимся, а затем делаем оптимальное количество свопы сортировать массив.

Чтобы применить это, мы используем немного магии шаблонов, чтобы вывести длину массива и применить правильную сеть:

#include <iostream>
using namespace std;

template< int K >
void static_sort(const double(&array)[K])
{
    cout << "General static sort\n" << endl;
}

template<>
void static_sort<3>(const double(&array)[3])
{
    cout << "Static sort for K=3" << endl;
}


int main()
{

    double  array[3];

    // performance critical code.
    // ...
    static_sort(array);
    // ...

}

Очевидно, что это довольно сложно, чтобы закодировать все это, поэтому:

  • Есть ли у кого-нибудь мнения о том, стоит ли это делать?
  • Кто-нибудь знает, существует ли такая оптимизация в любых стандартных реализациях, например, std:: sort?
  • Есть ли свободное место для реализации кода, реализующего такую ​​сортировочную сеть?
  • Возможно, можно было бы создать такую ​​сортирующую сеть, как это статически, используя магию шаблонов.

Теперь я просто использую сортировку вставки с параметром статического шаблона (как указано выше), в надежде, что он будет поощрять разворачивание и другие оптимизации времени компиляции.

Ваши мысли приветствуются.


Update: Я написал некоторый тестовый код для сравнения "статической" вставки short и std:: sort. (Когда я говорю "статический", я имею в виду, что размер массива фиксирован и выведен во время компиляции (предположительно, позволяет разворачивать цикл и т.д.). Я получаю хотя бы 20% улучшение NET (обратите внимание, что генерация включена в выборку). Платформа: clang, OS X 10.9.

Код здесь https://github.com/rosshemsley/static_sorting, если вы хотите сравнить его с вашими реализациями stdlib.

Мне еще предстоит найти хороший набор реализаций для сортировщиков сетевых компараторов.


4b9b3361

Ответ 1

Известны оптимальные или, по меньшей мере, наилучшие компараторы длины для N < 16, поэтому существует, по меньшей мере, довольно хорошая стартовая точка. Справедливо, поскольку оптимальные сети не обязательно предназначены для максимального уровня parallelism, достижимого, например. SSE или другой векторной арифметики.

Другое дело, что уже некоторые оптимальные сети для некоторого N являются вырожденными версиями для немного большей оптимальной сети для N + 1.

От wikipedia:

Известны оптимальные глубины для до 10 входов, и они соответственно 0, 1, 3, 3, 5, 5, 6, 6, 7, 7.

Это говорит о том, что я буду добиваться реализации сетей для N = {4, 6, 8 и 10}, так как ограничение глубины не может быть смоделировано дополнительным parallelism (я думаю). Я также думаю, что способность работать в регистре SSE (также используя некоторые инструкции min/max) или даже некоторый относительно большой набор регистров в архитектуре RISC обеспечит заметное преимущество в производительности по сравнению с "хорошо известными" методами сортировки, такими как quicksort из-за отсутствие арифметики указателя и другие накладные расходы.

Кроме того, я преследовал цель реализовать параллельную сеть, используя трюк для разгромного цикла устройство Duff.

ИЗМЕНИТЬ Когда известно, что входные значения положительные IEEE-754 плавают или удваиваются, стоит также упомянуть, что сравнение может также выполняться как целые числа. (float и int должны иметь одинаковую точность)

Ответ 2

Другие ответы интересны и довольно хороши, но я считаю, что могу предоставить некоторые дополнительные элементы ответа, точки за точку:

  • Это стоит усилий? Ну, если вам нужно отсортировать небольшие коллекции целых чисел, и сортировочные сети настроены так, чтобы максимально использовать некоторые инструкции, это может стоить усилий. На следующем графике представлены результаты сортировки миллиона массивов int размером 0-14 с различными алгоритмами сортировки. Как вы можете видеть, сортировочные сети могут обеспечить значительное ускорение, если вам это действительно нужно.

  • Нет стандартной реализации std::sort Я знаю, как использовать сортировочные сети; когда они не будут точно настроены, они могут быть медленнее, чем прямой сортировка вставки. libС++ std::sort имеет выделенные алгоритмы для сортировки от 0 до 5 значений одновременно, но они также не используют сортировочные сети. Единственный алгоритм сортировки, о котором я знаю, который использует сортировочные сети для сортировки нескольких значений, Wikisort. Тем не менее, исследовательский документ Применение сортировочных сетей для синтеза оптимизированных сортировочных библиотек предполагает, что сортировочные сети могут использоваться для сортировки небольших массивов или для улучшения алгоритмов рекурсивной сортировки таких как quicksort, но только если они настроены на то, чтобы использовать конкретные аппаратные инструкции.

    Алгоритм access aligned sort - это своего рода восходящая слияния, которая, по-видимому, использует битовые сети сортировки, реализованные с инструкциями SIMD для первого прохода. По-видимому, алгоритм может быть быстрее, чем стандартная библиотека для некоторых скалярных типов.

  • Я могу фактически предоставить такую ​​информацию по той простой причине, что я разработал библиотеку сортировки С++ 14, которая, как представляется, обеспечивает эффективную сортировочные сети размером от 0 до 32, которые реализуют оптимизации, описанные в предыдущем разделе. Я использовал его для генерации графика в первом разделе. Я все еще работаю над сетью сортировки в библиотеке, чтобы обеспечить оптимальные по размеру, оптимальные по глубине и оптимальные по свопам сети. Малые сети оптимальной сортировки найдены с грубой силой, а большие сортировочные сети используют результаты из-за литеровки.

    Обратите внимание: ни один из алгоритмов сортировки в библиотеке напрямую не использует сортировочные сети, но вы можете их адаптировать, чтобы сортировочная сеть была выбрана всякий раз, когда алгоритму сортировки задан небольшой std::array или небольшой массив C с фиксированным размером

    using namespace cppsort;
    
    // Sorters are function objects that can be
    // adapted with sorter adapters from the
    // library
    using sorter = small_array_adapter<
        std_sorter,
        sorting_network_sorter
    >;
    
    // Now you can use it as a function
    sorter sort;
    
    // Instead of a size-agnostic sorting algorithm,
    // sort will use an optimal sorting network for
    // 5 inputs since the bound of the array can be
    // deduced at compile time
    int arr[] = { 2, 4, 7, 9, 3 };
    sort(arr);
    

    Как уже упоминалось выше, библиотека обеспечивает эффективные сети сортировки для встроенных целых чисел, но вам, вероятно, не повезло, если вам нужно сортировать небольшие массивы чего-то другого (например, мои последние тесты показывают, что они не лучше, чем прямая вставка сортировка даже для long long int).

  • Возможно, вы можете использовать метапрограммирование шаблонов для создания сортировочных сетей любого размера, но ни один известный алгоритм не может генерировать лучшие сортировочные сети, поэтому вы также можете написать лучшие из них вручную. Я не думаю, что те, которые генерируются с помощью простых алгоритмов, могут фактически обеспечить пригодные для использования и эффективные сети в любом случае (нечетные и нечетные сортировочные и парные сети сортировки могут быть единственными полезными) [Другой ответ, похоже, показывает, что сгенерированные сети могут действительно работать].

Ответ 3

Недавно я написал небольшой класс, который использует алгоритм Бозе-Нельсона для создания сортировочной сети во время компиляции.

/**
 * A Functor class to create a sort for fixed sized arrays/containers with a
 * compile time generated Bose-Nelson sorting network.
 * \tparam NumElements  The number of elements in the array or container to sort.
 * \tparam T            The element type.
 * \tparam Compare      A comparator functor class that returns true if lhs < rhs.
 */
template <unsigned NumElements, class Compare = void> class StaticSort
{
    template <class A, class C> struct Swap
    {
        template <class T> inline void s(T &v0, T &v1)
        {
            T t = Compare()(v0, v1) ? v0 : v1; // Min
            v1 = Compare()(v0, v1) ? v1 : v0; // Max
            v0 = t;
        }

        inline Swap(A &a, const int &i0, const int &i1) { s(a[i0], a[i1]); }
    };

    template <class A> struct Swap <A, void>
    {
        template <class T> inline void s(T &v0, T &v1)
        {
            // Explicitly code out the Min and Max to nudge the compiler
            // to generate branchless code.
            T t = v0 < v1 ? v0 : v1; // Min
            v1 = v0 < v1 ? v1 : v0; // Max
            v0 = t;
        }

        inline Swap(A &a, const int &i0, const int &i1) { s(a[i0], a[i1]); }
    };

    template <class A, class C, int I, int J, int X, int Y> struct PB
    {
        inline PB(A &a)
        {
            enum { L = X >> 1, M = (X & 1 ? Y : Y + 1) >> 1, IAddL = I + L, XSubL = X - L };
            PB<A, C, I, J, L, M> p0(a);
            PB<A, C, IAddL, J + M, XSubL, Y - M> p1(a);
            PB<A, C, IAddL, J, XSubL, M> p2(a);
        }
    };

    template <class A, class C, int I, int J> struct PB <A, C, I, J, 1, 1>
    {
        inline PB(A &a) { Swap<A, C> s(a, I - 1, J - 1); }
    };

    template <class A, class C, int I, int J> struct PB <A, C, I, J, 1, 2>
    {
        inline PB(A &a) { Swap<A, C> s0(a, I - 1, J); Swap<A, C> s1(a, I - 1, J - 1); }
    };

    template <class A, class C, int I, int J> struct PB <A, C, I, J, 2, 1>
    {
        inline PB(A &a) { Swap<A, C> s0(a, I - 1, J - 1); Swap<A, C> s1(a, I, J - 1); }
    };

    template <class A, class C, int I, int M, bool Stop = false> struct PS
    {
        inline PS(A &a)
        {
            enum { L = M >> 1, IAddL = I + L, MSubL = M - L};
            PS<A, C, I, L, (L <= 1)> ps0(a);
            PS<A, C, IAddL, MSubL, (MSubL <= 1)> ps1(a);
            PB<A, C, I, IAddL, L, MSubL> pb(a);
        }
    };

    template <class A, class C, int I, int M> struct PS <A, C, I, M, true>
    {
        inline PS(A &a) {}
    };

public:
    /**
     * Sorts the array/container arr.
     * \param  arr  The array/container to be sorted.
     */
    template <class Container> inline void operator() (Container &arr) const
    {
        PS<Container, Compare, 1, NumElements, (NumElements <= 1)> ps(arr);
    };

    /**
     * Sorts the array arr.
     * \param  arr  The array to be sorted.
     */
    template <class T> inline void operator() (T *arr) const
    {
        PS<T*, Compare, 1, NumElements, (NumElements <= 1)> ps(arr);
    };
};

#include <iostream>
#include <vector>

int main(int argc, const char * argv[])
{
    enum { NumValues = 32 };

    // Arrays
    {
        int rands[NumValues];
        for (int i = 0; i < NumValues; ++i) rands[i] = rand() % 100;
        std::cout << "Before Sort: \t";
        for (int i = 0; i < NumValues; ++i) std::cout << rands[i] << " ";
        std::cout << "\n";
        StaticSort<NumValues> staticSort;
        staticSort(rands);
        std::cout << "After Sort: \t";
        for (int i = 0; i < NumValues; ++i) std::cout << rands[i] << " ";
        std::cout << "\n";
    }

    std::cout << "\n";

    // STL Vector
    {
        std::vector<int> rands(NumValues);
        for (int i = 0; i < NumValues; ++i) rands[i] = rand() % 100;
        std::cout << "Before Sort: \t";
        for (int i = 0; i < NumValues; ++i) std::cout << rands[i] << " ";
        std::cout << "\n";
        StaticSort<NumValues> staticSort;
        staticSort(rands);
        std::cout << "After Sort: \t";
        for (int i = 0; i < NumValues; ++i) std::cout << rands[i] << " ";
        std::cout << "\n";
    }

    return 0;
}

Бенчмарки

Следующие тесты собираются с помощью clang -O3 и запускаются в моем macbook в середине 2012 года.

Время (в миллисекундах) для сортировки 1 миллиона массивов.
Количество миллисекунд для массивов размером 2, 4, 8 равно 1.943, 8.655, 20.246 соответственно.
С++ Templated Bose-Nelson Static Sort Timings

Здесь приведены средние часы в сортировке для небольших массивов из 6 элементов. Базовый код и примеры можно найти по этому вопросу:
Самый быстрый тип фиксированной длины 6 int array

Direct call to qsort library function   : 342.26
Naive implementation (insertion sort)   : 136.76
Insertion Sort (Daniel Stutzbach)       : 101.37
Insertion Sort Unrolled                 : 110.27
Rank Order                              : 90.88
Rank Order with registers               : 90.29
Sorting Networks (Daniel Stutzbach)     : 93.66
Sorting Networks (Paul R)               : 31.54
Sorting Networks 12 with Fast Swap      : 32.06
Sorting Networks 12 reordered Swap      : 29.74
Reordered Sorting Network w/ fast swap  : 25.28
Templated Sorting Network (this class)  : 25.01 

Он работает так же быстро, как самый быстрый пример в вопросе для 6 элементов.

Код, используемый для эталонных тестов, можно найти здесь.

Ответ 4

Позвольте мне поделиться некоторыми мыслями.

Есть ли у кого-нибудь мнения о том, стоит ли это усилия?

Невозможно дать правильный ответ. Вы должны просмотреть свой фактический код, чтобы узнать это. В моей практике, когда дело доходит до низкоуровневого профилирования, узкое место всегда было не там, где я думал.

Кто-нибудь знает, существует ли такая оптимизация в любом стандарте реализации, например, std:: sort?

Например, реализация Visual С++ std::sort использует сортировку вставки для небольших векторов. Я не знаю о реализации, которая использует оптимальные сети сортировки.

Возможно, можно создать такую ​​сортирующую сеть, как это статически используя шаблонную магию

Существуют алгоритмы для генерации сортировочных сетей, таких как алгоритмы Бозе-Нельсона, Хиббарда и Бэтчера. Поскольку шаблоны С++ являются Turing-complete, вы можете реализовать их с помощью TMP. Однако эти алгоритмы не гарантируют теоретически минимальное количество компараторов, поэтому вы можете запрограммировать оптимальную сеть.