使用比较器网络快速排序固定长度阵列

时间:2013-11-05 13:48:07

标签: c++ arrays sorting template-meta-programming sorting-network

我有一些性能关键代码,涉及在C ++中对大小为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?
  • 是否有一个容易实现这种排序网络的代码?
  • 也许有可能使用模板魔法静态生成这样的排序网络。

现在我只使用带有静态模板参数的插入排序(如上所述),希望它能够鼓励展开和其他编译时优化。

欢迎您的到来。


更新 的 我写了一些测试代码来比较'static'插入short和std :: sort。 (当我说静态时,我的意思是数组大小是固定的并在编译时推导出来(可能是允许循环展开等)。 我得到至少20%的净改善(请注意,这一代包括在时间中)。平台:clang,OS X 10.9。

如果您想将此代码与stdlib的实现进行比较,则代码在https://github.com/rosshemsley/static_sorting

我还没有为比较器网络分拣机找到一套很好的实现。


4 个答案:

答案 0 :(得分:12)

我最近写了一个小类,它使用Bose-Nelson算法在编译时生成排序网络。

/**
 * 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编译并在我2012年中期的macbook air上运行。

对100万个数组进行排序的时间(以毫秒为单位) 大小为2,4,8的数组的毫秒数分别为1.943,8.655,20.246 C++ Templated Bose-Nelson Static Sort timings

以下是6个元素的小数组的每种平均时钟数。基准代码和示例可以在这个问题上找到:
Fastest sort of fixed length 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个元素的问题中最快的例子一样快。

可以找到用于基准测试的代码here

答案 1 :(得分:9)

其他答案很有趣且相当不错,但我相信我可以提供一些额外的答案元素,每点指出:

  • 值得努力吗?好吧,如果你需要对整数的小集合进行排序,并且调整排序网络以尽可能地利用一些指令,那么它可能是值得的。下图显示了使用不同排序算法对一百万个大小为0-14的int数组进行排序的结果。如您所见,如果您确实需要,排序网络可以提供显着的加速。

  • std::sort没有标准实现我知道使用排序网络;当它们没有经过微调时,它们可能比直接插入类型慢。 libc ++的std::sort具有专用算法,可以同时对0到5的值进行排序,但它们也不使用排序网络。我所知道的唯一使用排序网络对几个值进行排序的排序算法是Wikisort。也就是说,研究论文Applying Sorting Networks to Synthesize Optimized Sorting Libraries表明排序网络可用于对小型数组进行排序或改进递归排序算法(如快速排序),但前提是它们经过微调以利用特定的硬件指令。

    access aligned sort算法是某种自下而上的mergesort,它显然使用了用第一遍的SIMD指令实现的bitonic排序网络。显然,对于某些标量类型,算法可能比标准库更快。

  • 我实际上可以提供这样的信息,原因很简单,我开发a C++14 sorting library恰好提供了大小为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)。

  • 你可以使用模板元编程来生成任何大小的排序网络,但是没有已知的算法可以生成最好的排序网络,所以你也可以手工编写最好的排序网络。我不认为由简单算法生成的那些实际上可以提供可用且有效的网络(Batcher的奇偶排序和成对排序网络可能是唯一可用的网络) [另一个答案似乎表明生成的网络实际上可以工作

答案 2 :(得分:8)

对于N <16,已知最佳或至少最佳长度的比较器网络,因此至少有一个相当好的起点。相当地,因为最佳网络不一定是针对例如用以下实现的最大水平的并行性而设计的。 SSE或其他矢量算术。

另一点是,某些N的某些最优网络是N + 1略大的最佳网络的简并版本。

来自wikipedia

  

最多10个输入的最佳深度是已知的,它们是   分别为0,1,3,3,5,5,6,6,7,7。

这就是说,我追求实现N = {4,6,8和10}的网络,因为深度约束不能通过额外的并行性来模拟(我认为)。我还认为,与“众所周知”的排序方法(例如快速排序)相比,在RISC架构中使用SSE寄存器(也使用一些最小/最大指令)或甚至一些相对较大的寄存器设置的能力将提供显着的性能优势。没有指针算术和其他开销。

此外,我还希望使用臭名昭着的循环展开技巧Duff's device来实现并行网络。

修改 当已知输入值为正IEEE-754浮点数或双精度数时,还值得一提的是,比较也可以作为整数执行。 (float和int必须具有相同的字节顺序)

答案 3 :(得分:3)

让我分享一些想法。

  

有没有人对这是否值得这样做有任何意见   努力?

无法给出正确答案。您必须分析您的实际代码才能找到它。 在我的实践中,当谈到低级别的分析时,瓶颈总是不在我想的地方。

  

有谁知道这种优化是否存在于任何标准中   例如,std :: sort?

的实现

例如,std::sort的Visual C ++实现使用小向量的插入排序。我不知道使用最佳排序网络的实现。

  

也许有可能生成这样的排序网络   静态使用模板魔法

有生成排序网络的算法,例如Bose-Nelson,Hibbard和Batcher的算法。由于C ++模板是Turing-complete,您可以使用TMP实现它们。但是,这些算法无法保证在理论上给出最小数量的比较器,因此您可能需要对最佳网络进行硬编码。