27

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 などの標準実装に存在するかどうかは誰にもわかりませんか?
  • この種の並べ替えネットワークを実装するコードを簡単に入手できる場所はありますか?
  • おそらく、テンプレート マジックを使用して静的にこのような並べ替えネットワークを生成することは可能でしょう..

今のところ、展開やその他のコンパイル時の最適化を促進することを期待して、(上記のように) 静的テンプレート パラメーターを使用して挿入並べ替えを使用するだけです。

あなたの考えを歓迎します。


更新: 「静的」挿入ショートと std::sort を比較するテスト コードをいくつか書きました。(静的とは、配列のサイズが固定され、コンパイル時に推定されることを意味します (おそらくループのアンローリングなどを許可します)。少なくとも 20% の NET の改善が得られます (世代がタイミングに含まれることに注意してください)。プラットフォーム:クラン、OS X 10.9。

stdlib の実装と比較したい場合、コードはhttps://github.com/rosshemsley/static_sortingにあります。

私はまだ、コンパレータ ネットワーク ソーターの優れた実装セットを見つけていません。


4

4 に答える 4

18

これは、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++ テンプレート化された Bose-Nelson 静的ソートのタイミング

以下は、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 要素の質問で最速の例と同じくらい高速に実行されます。

ベンチマークに使用されたコードは、ここにあります。

これには、より多くの機能とさらなる最適化が含まれており、実世界のデータでより堅牢なパフォーマンスが得られます。

于 2016-03-24T16:22:00.227 に答える
10

他の回答は興味深く、かなり良いですが、回答のいくつかの追加要素をポイントごとに提供できると思います。

  • 努力する価値はありますか?整数の小さなコレクションを並べ替える必要があり、並べ替えネットワークがいくつかの命令を最大限に活用するように調整されている場合は、努力する価値があるかもしれません。次のグラフは、サイズが 0 ~ 14 の 100 万個の配列intをさまざまな並べ替えアルゴリズムで並べ替えた結果を示しています。ご覧のとおり、本当に必要な場合は、並べ替えネットワークによって大幅な高速化が実現されます。

  • std::sortI know of use ソート ネットワークの標準実装はありません。微調整されていない場合、直接挿入ソートよりも遅くなる可能性があります。libc++std::sortには、0 から 5 までの値を一度にソートするための専用アルゴリズムがありますが、ソート ネットワークも使用しません。ソート ネットワークを使用していくつかの値をソートする、私が知っている唯一のソート アルゴリズムはWikisortです。とはいえ、研究論文Applying Sorting Networks to Synthesize Optimized Sorting Librariesは、ソート ネットワークを使用して小さな配列をソートしたり、クイックソートなどの再帰的なソート アルゴリズムを改善したりできることを示唆していますが、特定のハードウェア命令を利用するように微調整されている場合に限られます。 .

    アクセス整列ソートアルゴリズムは、最初のパスに SIMD 命令で実装されたビットニックソートネットワークを明らかに使用する、ある種のボトムアップマージソートです。どうやら、一部のスカラー型では、アルゴリズムが標準ライブラリよりも高速になる可能性があります。

  • 前のセクションで説明した最適化を実装するサイズ 0 ~ 32 の効率的な並べ替えネットワークをたまたま提供するC++14 並べ替えライブラリを開発したという単純な理由で、実際にそのような情報を提供できます。最初のセクションでグラフを生成するために使用しました。サイズ最適化、深さ最適化、およびスワップ最適化ネットワークを提供するために、ライブラリのソート ネットワーク部分にまだ取り組んでいます。小規模な最適な並べ替えネットワークは力ずくで見つけられますが、より大きな並べ替えネットワークは文献からの結果を使用します。

    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 の奇数偶数ソートおよびペアワイズ ソート ネットワークだけが使用可能なネットワークである可能性があります) [別の回答は、生成されたネットワークが実際に機能する可能性があることを示しているようです] .

于 2016-02-17T14:47:16.480 に答える
8

N<16 に対して最適な、または少なくとも最適な長さ比較ネットワークが知られているため、少なくともかなり良い出発点があります。当然のことながら、最適なネットワークは、SSE やその他のベクトル演算などで達成可能な最大レベルの並列処理のために設計されているとは限りません。

もう 1 つのポイントは、いくつかの N に対するいくつかの最適なネットワークが、N+1 に対するわずかに大きい最適なネットワークの縮退バージョンであるということです。

ウィキペディアから:

最大 10 個の入力に対する最適な深さは既知であり、それぞれ 0、1、3、3、5、5、6、6、7、7 です。

これは、N = {4、6、8、および10}のネットワークを実装することを追求したいと思います。これは、深さの制約を追加の並列処理でシミュレートできないためです(私は思います)。また、SSE のレジスター (いくつかの最小/最大命令も使用) または RISC アーキテクチャーの比較的大きなレジスター・セットでさえも動作する機能は、クイックソートなどの「よく知られた」ソート方法と比較して、顕著なパフォーマンス上の利点を提供すると思います。ポインター演算やその他のオーバーヘッドがありません。

さらに、悪名高いループ展開トリックDuff's deviceを使用して並列ネットワークを実装することを追求したいと思います。

EDIT 入力値が正の IEEE-754 float または double であることがわかっている場合、比較は整数としても実行できることにも言及する価値があります。(float と int は同じエンディアンである必要があります)

于 2013-11-05T15:57:25.700 に答える