最長の固定長6整数配列


401

Stack Overflowの別の質問(この質問)に答えると、興味深い副問題に遭遇しました。6つの整数の配列をソートする最も速い方法は何ですか?

質問は非常に低いレベルなので:

  • ライブラリが利用可能である(および呼び出し自体にコストがかかる)とは想定できません。プレーンなCのみです。
  • (コストが非常に高い)命令パイプラインが空になるのを避けるために、分岐、ジャンプ、およびその他すべての種類の制御フローの中断(&&またはのシーケンスポイントの背後にあるものなど)を最小限に抑える必要があり||ます。
  • 部屋には制約があり、レジスタとメモリの使用を最小限に抑えることが問題です。理想的には、並べ替えが最善です。

本当にこの質問は、ソースの長さを最小化することではなく、実行時間を目標とする一種のゴルフです。マイケル・アブラッシュとその続編による『Zen of Code optimization』という本のタイトルで使用されているように、私はそれを「Zening」コードと呼んでいます。

なぜそれが興味深いのかについては、いくつかの層があります:

  • この例はシンプルで理解しやすく、測定も簡単で、Cのスキルはそれほど必要ありません
  • これは、問題に対して適切なアルゴリズムを選択した場合の効果だけでなく、コンパイラーと基礎となるハードウェアの効果も示しています。

これが私のリファレンス(素朴で最適化されていない)の実装とテストセットです。

#include <stdio.h>

static __inline__ int sort6(int * d){

    char j, i, imin;
    int tmp;
    for (j = 0 ; j < 5 ; j++){
        imin = j;
        for (i = j + 1; i < 6 ; i++){
            if (d[i] < d[imin]){
                imin = i;
            }
        }
        tmp = d[j];
        d[j] = d[imin];
        d[imin] = tmp;
    }
}

static __inline__ unsigned long long rdtsc(void)
{
  unsigned long long int x;
     __asm__ volatile (".byte 0x0f, 0x31" : "=A" (x));
     return x;
}

int main(int argc, char ** argv){
    int i;
    int d[6][5] = {
        {1, 2, 3, 4, 5, 6},
        {6, 5, 4, 3, 2, 1},
        {100, 2, 300, 4, 500, 6},
        {100, 2, 3, 4, 500, 6},
        {1, 200, 3, 4, 5, 600},
        {1, 1, 2, 1, 2, 1}
    };

    unsigned long long cycles = rdtsc();
    for (i = 0; i < 6 ; i++){
        sort6(d[i]);
        /*
         * printf("d%d : %d %d %d %d %d %d\n", i,
         *  d[i][0], d[i][6], d[i][7],
         *  d[i][8], d[i][9], d[i][10]);
        */
    }
    cycles = rdtsc() - cycles;
    printf("Time is %d\n", (unsigned)cycles);
}

生の結果

バリアントの数が増えてきているので、ここにあるテストスイートにそれらをすべて集めまし。Kevin Stockのおかげで、実際に使用したテストは、上記に示したテストよりも少し単純です。独自の環境でコンパイルして実行できます。さまざまなターゲットアーキテクチャ/コンパイラでの動作にかなり興味があります。(大丈夫です、答えに入れてください。私は新しい結果セットのすべての貢献者に+1します)。

1年前、ダニエルシュッツバッハ(ゴルフ用)に答えをあげました。彼がその時点で最速のソリューション(ソーティングネットワーク)のソースだったからです。

Linux 64ビット、gcc 4.6.1 64ビット、Intel Core 2 Duo E8400、-O2

  • qsortライブラリ関数の直接呼び出し:689.38
  • 単純な実装(挿入ソート):285.70
  • 挿入ソート(Daniel Stutzbach):142.12
  • 挿入ソートアンロール:125.47
  • ランク順:102.26
  • レジスタ付きのランク順:58.03
  • ソーティングネットワーク(Daniel Stutzbach):111.68
  • ソーティングネットワーク(Paul R):66.36
  • 高速スワップによるネットワーク12のソート:58.86
  • ソーティングネットワーク12再注文スワップ:53.74
  • ソーティングネットワーク12再注文シンプルスワップ:31.54
  • 高速スワップを使用した並べ替えネットワークの並べ替え:31.54
  • 再スワップされたソーティングネットワーク(高速スワップあり)V2:33.63
  • インラインバブルソート(Paolo Bonzini):48.85
  • アンロール挿入ソート(Paolo Bonzini):75.30

Linux 64ビット、gcc 4.6.1 64ビット、Intel Core 2 Duo E8400、-O1

  • qsortライブラリ関数の直接呼び出し:705.93
  • 素朴な実装(挿入ソート):135.60
  • 挿入ソート(Daniel Stutzbach):142.11
  • 挿入ソートアンロール:126.75
  • ランク順:46.42
  • レジスタ付きのランク順:43.58
  • ソーティングネットワーク(Daniel Stutzbach):115.57
  • ソーティングネットワーク(ポールR):64.44
  • 高速スワップによるネットワーク12のソート:61.98
  • ソーティングネットワーク12再注文スワップ:54.67
  • ソーティングネットワーク12再注文シンプルスワップ:31.54
  • 並べ替えネットワークの並べ替えを高速スワップで:31.24
  • 高速スワップV2を使用した並べ替えネットワークの並べ替え:33.07
  • インラインバブルソート(Paolo Bonzini):45.79
  • アンロール挿入ソート(Paolo Bonzini):80.15

驚くべきことに、いくつかのプログラムではO2はO1よりも効率が悪いため、-O1と-O2の両方の結果を含めました。この特定の最適化にはどのような効果がありますか?

提案されたソリューションに関するコメント

挿入ソート(Daniel Stutzbach)

予想通り、ブランチを最小化することは確かに良い考えです。

ソーティングネットワーク(Daniel Stutzbach)

挿入ソートよりも優れています。主な効果は外部ループを回避することから得られたのではないかと思いました。私はそれをチェックするために展開された挿入ソートで試してみました、そして確かに我々はほぼ同じ数字を得ます(コードはここにあります)。

ソーティングネットワーク(Paul R)

これまでで最高。テストに使用した実際のコードはこちらです。それが他のソーティングネットワーク実装のほぼ2倍の速度である理由はまだわかりません。パラメータ渡し?Fast max?

ソーティングネットワーク12 SWAP with Fast Swap

Daniel Stutzbachが示唆したように、私は彼の12スワップソーティングネットワークをブランチレス高速スワップと組み合わせました(コードはこちら)。これは確かに高速であり、1少ないスワップを使用して期待できるように、マージンがわずか(約5%)でこれまでのところ最高です。

また、ブランチレススワップは、PPCアーキテクチャのifを使用した単純なスワップよりも効率がはるかに(4倍)低いようです。

ライブラリqsortの呼び出し

別の参照ポイントを与えるために、ライブラリqsortを呼び出すように提案されたように試しました(コードはここにあります)。予想どおり、はるかに遅くなります。10〜30倍遅くなります...新しいテストスイートで明らかになったように、主な問題は、最初の呼び出し後のライブラリの初期ロードであり、他のライブラリと比べてそれほど悪くはないようですバージョン。私のLinuxでは3倍から20倍遅いだけです。他の人がテストに使用している一部のアーキテクチャでは、さらに高速であるように見えます(ライブラリqsortがより複雑なAPIを使用しているため、そのアーキテクチャには本当に驚きます)。

ランキング

Rex Kerrは、別の完全に異なる方法を提案しました。配列の各項目について、最終的な位置を直接計算します。ランク順の計算には分岐が必要ないため、これは効率的です。この方法の欠点は、配列のメモリ量の3倍(ランクの順序を格納するための配列と変数の1つのコピー)が必要なことです。パフォーマンスの結果は非常に驚くべきものです(そして興味深いものです)。32ビットOSとIntel Core2 Quad E8300を搭載した私のリファレンスアーキテクチャでは、サイクル数は1000をわずかに下回っていました(分岐スワップを使用したソートネットワークのように)。しかし、私の64ビットボックス(Intel Core2 Duo)でコンパイルして実行すると、パフォーマンスが大幅に向上しました。これまでで最速になりました。やっと本当の理由がわかりました。私の32ビットボックスはgcc 4.4.1を、64ビットボックスはgcc 4.4を使用しています。

更新

上記の公表された数値が示すように、この効果は後のバージョンのgccによってさらに強化され、ランクオーダーは他のどの方法よりも常に2倍速くなりました。

並べ替えられたスワップによるネットワーク12のソート

gcc 4.4.3でのRex Kerrの提案の驚くべき効率性は、3倍のメモリ使用量のプログラムがブランチレスソーティングネットワークよりも高速になるのはなぜだろうと思いました。私の仮説は、書き込み後に読み取る種類の依存性が少なく、x86のスーパースカラー命令スケジューラをより適切に使用できるようにすることでした。それは私にアイデアを与えました:書き込み後の依存関係を最小限に抑えるためにスワップを並べ替えます。より簡単に言うとSWAP(1, 2); SWAP(0, 2);、どちらも共通のメモリセルにアクセスするため、最初のスワップが完了するのを待ってから2番目のスワップを実行する必要があります。SWAP(1, 2); SWAP(4, 5);実行すると、プロセッサは両方を並行して実行できます。私はそれを試してみましたが、期待どおりに動作し、ソーティングネットワークは約10%高速で実行されています。

シンプルスワップによるネットワーク12の並べ替え

元の投稿であるSteinar H. Gundersonが提案してから1年後、コンパイラーの裏をかいて、スワップコードを単純に保つことはしないでください。結果のコードは約40%高速であるため、これは確かに良い考えです。彼はまた、x86インラインアセンブリコードを使用して手動で最適化されたスワップを提案しました。最も驚くべきことは(プログラマの心理学のボリュームを言っている)、1年前に、誰もそのバージョンのスワップを試していないことです。テストに使用したコードはこちらです。C高速スワップを作成する他の方法を提案する人もいますが、それはまともなコンパイラーを使用した単純なものと同じパフォーマンスをもたらします。

「最良の」コードは次のとおりです。

static inline void sort6_sorting_network_simple_swap(int * d){
#define min(x, y) (x<y?x:y)
#define max(x, y) (x<y?y:x) 
#define SWAP(x,y) { const int a = min(d[x], d[y]); \
                    const int b = max(d[x], d[y]); \
                    d[x] = a; d[y] = b; }
    SWAP(1, 2);
    SWAP(4, 5);
    SWAP(0, 2);
    SWAP(3, 5);
    SWAP(0, 1);
    SWAP(3, 4);
    SWAP(1, 4);
    SWAP(0, 3);
    SWAP(2, 5);
    SWAP(1, 3);
    SWAP(2, 4);
    SWAP(2, 3);
#undef SWAP
#undef min
#undef max
}

私たちのテストセットを信じている場合(そして、はい、それはかなり貧弱です、それは単なる利点であり、測定対象を理解するのが簡単であることです)、1つのソートの結果のコードの平均サイクル数は40サイクル未満です( 6つのテストが実行されます)。これにより、各スワップは平均4サイクルになります。私はそれを驚くほど速く呼びます。他に可能な改善はありますか?


2
intにいくつかの制約がありますか?たとえば、2 x、y x-yx+yアンダーフローやオーバーフローが発生しないと想定できますか?
Matthieu M.

3
私の12スワップソーティングネットワークとPaulのブランチレススワップ機能を組み合わせてみてください。彼のソリューションは、配列への単一のポインターではなく、スタック上の個別の要素としてすべてのパラメーターを渡します。それも違いを生むかもしれません。
Daniel Stutzbach、2010年

2
64ビットでのrdtscの正しい実装は__asm__ volatile (".byte 0x0f, 0x31; shlq $32, %%rdx; orq %%rdx, %0" : "=a" (x) : : "rdx");、GCCが単一の64ビットレジスタに期待するのに対して、rdtscがEDX:EAXに回答を置くためです。-O3でコンパイルすると、バグを確認できます。より速いSWAPについてのPaul Rへの私のコメントも以下を参照してください。
Paolo Bonzini、2011

3
@タイラー:ブランチなしでアセンブリレベルでどのように実装しますか?
Loren Pechtel、2011

4
@Loren:がより大きいか小さいかによって、それぞれCMP EAX, EBX; SBB EAX, EAX0または0xFFFFFFFFを入れます。 (「キャリー付き加算」)に対応する「借用付き減算」です。ステータスは、参照ビットキャリービット。繰り返しになりますが、Pentium 4 とでのレイテンシとスループットはひどいものでしたが、Core CPUでは2倍の速度でした。80386以降、条件付きストアと条件付き移動の命令もありますが、速度も遅いです。EAXEAXEBXSBBADCADCSBBADDSUBSETccCMOVcc
j_random_hacker '25

回答:


162

最適化については、テスト、テスト、テストが常に最善です。少なくともネットワークのソートと挿入のソートを試してみます。私が賭けていたら、私は過去の経験に基づいて挿入ソートにお金をかけます。

入力データについて何か知っていますか?一部のアルゴリズムでは、特定の種類のデータを使用するとパフォーマンスが向上します。たとえば、挿入ソートは、ソートされたデータまたはほぼソートされたデータに対してより優れたパフォーマンスを発揮するため、ほぼソートされたデータの確率が平均を超える場合は、より適切な選択になります。

投稿したアルゴリズムは挿入ソートに似ていますが、比較の回数を増やしてスワップ数を最小限に抑えているようです。ただし、ブランチは命令パイプラインを停止させる可能性があるため、比較はスワップよりもはるかにコストがかかります。

これは挿入ソートの実装です:

static __inline__ int sort6(int *d){
        int i, j;
        for (i = 1; i < 6; i++) {
                int tmp = d[i];
                for (j = i; j >= 1 && tmp < d[j-1]; j--)
                        d[j] = d[j-1];
                d[j] = tmp;
        }
}

これが、ソーティングネットワークを構築する方法です。まず、このサイトを使用し、適切な長さのネットワーク用の最小限のSWAPマクロセットを生成します。これを関数にラップすると、次のようになります。

static __inline__ int sort6(int * d){
#define SWAP(x,y) if (d[y] < d[x]) { int tmp = d[x]; d[x] = d[y]; d[y] = tmp; }
    SWAP(1, 2);
    SWAP(0, 2);
    SWAP(0, 1);
    SWAP(4, 5);
    SWAP(3, 5);
    SWAP(3, 4);
    SWAP(0, 3);
    SWAP(1, 4);
    SWAP(2, 5);
    SWAP(2, 4);
    SWAP(1, 3);
    SWAP(2, 3);
#undef SWAP
}

9
+1:いいですね。上記の手作業でコーディングして経験的に導き出したネットワークでは、13回ではなく12回の交換でやりました。あなたにネットワークを生成するサイトへのリンクができれば、もう1つ+1を差し上げます。ブックマークを付けました。
ポールR

9
リクエストの大部分が小さなサイズの配列であることが予想される場合、これは汎用ソート関数にとって素晴らしいアイデアです。この手順を使用して、最適化するケースのスイッチステートメントを使用します。デフォルトのケースでは、ライブラリのソート関数を使用します。
Mark Ransom

5
@Mark 優れたライブラリソート関数には、小さな配列の高速パスがすでにあります。最新のライブラリの多くは、に再帰した後にInsertionSortに切り替える再帰的なQuickSortまたはMergeSortを使用しますn < SMALL_CONSTANT
Daniel Stutzbach、2010年

3
@Markまあ、Cライブラリのソート関数では、関数ポーターを介して比較演算を指定する必要があります。すべての比較で関数を呼び出すオーバーヘッドは非常に大きくなります。通常、これは最もクリーンな方法です。これがプログラムのクリティカルパスになることはほとんどないためです。ただし、それがクリティカルパスである場合、整数とその正確に6つを並べ替えていることがわかっていれば、より速く並べ替えることができます。:)
Daniel Stutzbach、2010年

7
@tgwh:XORスワップはほとんど常に悪い考えです。
ポールR

63

これは、ソートネットワークを使用した実装です

inline void Sort2(int *p0, int *p1)
{
    const int temp = min(*p0, *p1);
    *p1 = max(*p0, *p1);
    *p0 = temp;
}

inline void Sort3(int *p0, int *p1, int *p2)
{
    Sort2(p0, p1);
    Sort2(p1, p2);
    Sort2(p0, p1);
}

inline void Sort4(int *p0, int *p1, int *p2, int *p3)
{
    Sort2(p0, p1);
    Sort2(p2, p3);
    Sort2(p0, p2);  
    Sort2(p1, p3);  
    Sort2(p1, p2);  
}

inline void Sort6(int *p0, int *p1, int *p2, int *p3, int *p4, int *p5)
{
    Sort3(p0, p1, p2);
    Sort3(p3, p4, p5);
    Sort2(p0, p3);  
    Sort2(p2, p5);  
    Sort4(p1, p2, p3, p4);  
}

これには、非常に効率的なブランチレスminmax実装が本当に必要です。それは、このコードが効果的にまとめたものであるということです-シーケンスminmax操作(合計13個)。これは読者の練習問題として残しておきます。

この実装は、簡単にベクトル化(たとえば、SIMD-ほとんどのSIMD ISAはベクトルの最小/最大命令を備えています)や、GPU実装(たとえば、CUDA-ブランチレスであるため、ワープの発散などの問題はありません)に適しています。

参照:非常に小さなリストをソートするための高速アルゴリズム実装


1
min / maxのビットハッキングの場合:graphics.stanford.edu/~seander/bithacks.html#IntegerMinOrMax
Rubys

1
@Paul:実際のCUDAの使用状況では、これが確かに最良の答えです。ゴルフx64のコンテキストにも含まれているかどうか(およびその程度)を確認し、結果を公開します。
クリス

1
Sort3あなたがそれ(a+b+c)-(min+max)が中心的な数字であることに気づいたら、(とにかくほとんどのアーキテクチャで)より速くなるでしょう。
Rex Kerr、

1
@レックス:なるほど-よさそうです。AltiVecやSSEのようなSIMDアーキテクチャでは、同じ数の命令サイクルになります(最大と最小は加算/減算のような単一サイクル命令です)が、通常のスカラーCPUの場合、メソッドの方が良く見えます。
ポールR

2
条件付きの移動命令でGCCにminを最適化させると、33%高速化します#define SWAP(x,y) { int dx = d[x], dy = d[y], tmp; tmp = d[x] = dx < dy ? dx : dy; d[y] ^= dx ^ tmp; }。ここで私は?:を使用していません。d[y]の場合、パフォーマンスがわずかに低下するためですが、ほとんどノイズの中にあります。
Paolo Bonzini、2011

45

これらは整数であり、比較は高速なので、それぞれの順位を直接計算してみませんか。

inline void sort6(int *d) {
  int e[6];
  memcpy(e,d,6*sizeof(int));
  int o0 = (d[0]>d[1])+(d[0]>d[2])+(d[0]>d[3])+(d[0]>d[4])+(d[0]>d[5]);
  int o1 = (d[1]>=d[0])+(d[1]>d[2])+(d[1]>d[3])+(d[1]>d[4])+(d[1]>d[5]);
  int o2 = (d[2]>=d[0])+(d[2]>=d[1])+(d[2]>d[3])+(d[2]>d[4])+(d[2]>d[5]);
  int o3 = (d[3]>=d[0])+(d[3]>=d[1])+(d[3]>=d[2])+(d[3]>d[4])+(d[3]>d[5]);
  int o4 = (d[4]>=d[0])+(d[4]>=d[1])+(d[4]>=d[2])+(d[4]>=d[3])+(d[4]>d[5]);
  int o5 = 15-(o0+o1+o2+o3+o4);
  d[o0]=e[0]; d[o1]=e[1]; d[o2]=e[2]; d[o3]=e[3]; d[o4]=e[4]; d[o5]=e[5];
}

@Rex:gcc -O1を使用すると、1000サイクル未満になり、ソートネットワークよりも高速ですが低速です。コードを改善するアイデアはありますか?たぶん、配列のコピーを避けることができれば…
kriss

@kriss:-O2を使用した場合のソートネットワークよりも高速です。-O2が大丈夫ではない理由があるのでしょうか、それとも-O2でも遅いのですか?たぶんそれはマシンアーキテクチャの違いですか?
レックスカー

1
@Rex:すみません、> vs> =パターンを一目で見逃しました。それはすべての場合で動作します。
クリス

3
@kriss:ああ。それは完全に驚くべきことではありません。たくさんの変数が浮かんでいるため、それらを注意深く順序付けし、レジスターなどにキャッシュする必要があります。
レックスカー

2
@SSpoke 0+1+2+3+4+5=15それらの1つが欠落しているため、15から残りの合計から1つが欠落します
Glenn Teitelbaum

35

一年遅れでパーティーに行ったようですが、ここに行きます...

gcc 4.5.2で生成されたアセンブリを見ると、スワップごとにロードとストアが行われていることがわかりましたが、これは実際には必要ありません。6つの値をレジスターにロードし、それらをソートして、それらをメモリーに格納する方がよいでしょう。レジスターが最初に必要になり、最後に使用されるように、ストアでのロードを可能な限りそこに近づけるように命令しました。Steinar H. GundersonのSWAPマクロも使用しました。更新:gccがGundersonのようなものに変換するPaolo BonziniのSWAPマクロに切り替えましたが、gccは明示的なアセンブリとして指定されていないため、命令をより適切に順序付けることができます。

並べ替えたスワップネットワークと同じスワップ順序を使用しました。さらに時間があれば、一連の順列を生成してテストします。

テストコードを変更して、4000を超える配列を検討し、それぞれを並べ替えるのに必要な平均サイクル数を示しました。i5-650では、(-O3を使用して)元の並べ替えネットワークが〜65.3サイクル/ソートを取得している(-O1を使用して-O2と-O3をビートしている)のと比較して、34.1サイクル/ソートを取得しています(-O3を使用)。

#include <stdio.h>

static inline void sort6_fast(int * d) {
#define SWAP(x,y) { int dx = x, dy = y, tmp; tmp = x = dx < dy ? dx : dy; y ^= dx ^ tmp; }
    register int x0,x1,x2,x3,x4,x5;
    x1 = d[1];
    x2 = d[2];
    SWAP(x1, x2);
    x4 = d[4];
    x5 = d[5];
    SWAP(x4, x5);
    x0 = d[0];
    SWAP(x0, x2);
    x3 = d[3];
    SWAP(x3, x5);
    SWAP(x0, x1);
    SWAP(x3, x4);
    SWAP(x1, x4);
    SWAP(x0, x3);
    d[0] = x0;
    SWAP(x2, x5);
    d[5] = x5;
    SWAP(x1, x3);
    d[1] = x1;
    SWAP(x2, x4);
    d[4] = x4;
    SWAP(x2, x3);
    d[2] = x2;
    d[3] = x3;

#undef SWAP
#undef min
#undef max
}

static __inline__ unsigned long long rdtsc(void)
{
    unsigned long long int x;
    __asm__ volatile ("rdtsc; shlq $32, %%rdx; orq %%rdx, %0" : "=a" (x) : : "rdx");
    return x;
}

void ran_fill(int n, int *a) {
    static int seed = 76521;
    while (n--) *a++ = (seed = seed *1812433253 + 12345);
}

#define NTESTS 4096
int main() {
    int i;
    int d[6*NTESTS];
    ran_fill(6*NTESTS, d);

    unsigned long long cycles = rdtsc();
    for (i = 0; i < 6*NTESTS ; i+=6) {
        sort6_fast(d+i);
    }
    cycles = rdtsc() - cycles;
    printf("Time is %.2lf\n", (double)cycles/(double)NTESTS);

    for (i = 0; i < 6*NTESTS ; i+=6) {
        if (d[i+0] > d[i+1] || d[i+1] > d[i+2] || d[i+2] > d[i+3] || d[i+3] > d[i+4] || d[i+4] > d[i+5])
            printf("d%d : %d %d %d %d %d %d\n", i,
                    d[i+0], d[i+1], d[i+2],
                    d[i+3], d[i+4], d[i+5]);
    }
    return 0;
}

変更されたテストスイート変更して、ソートごとのクロックもレポートし、より多くのテストを実行します(cmp関数は整数オーバーフローも処理するように更新されました)。いくつかの異なるアーキテクチャでの結果を以下に示します。AMD CPUでテストを試みましたが、入手可能なX6 1100Tではrdtscが信頼できません。

Clarkdale (i5-650)
==================
Direct call to qsort library function      635.14   575.65   581.61   577.76   521.12
Naive implementation (insertion sort)      538.30   135.36   134.89   240.62   101.23
Insertion Sort (Daniel Stutzbach)          424.48   159.85   160.76   152.01   151.92
Insertion Sort Unrolled                    339.16   125.16   125.81   129.93   123.16
Rank Order                                 184.34   106.58   54.74    93.24    94.09
Rank Order with registers                  127.45   104.65   53.79    98.05    97.95
Sorting Networks (Daniel Stutzbach)        269.77   130.56   128.15   126.70   127.30
Sorting Networks (Paul R)                  551.64   103.20   64.57    73.68    73.51
Sorting Networks 12 with Fast Swap         321.74   61.61    63.90    67.92    67.76
Sorting Networks 12 reordered Swap         318.75   60.69    65.90    70.25    70.06
Reordered Sorting Network w/ fast swap     145.91   34.17    32.66    32.22    32.18

Kentsfield (Core 2 Quad)
========================
Direct call to qsort library function      870.01   736.39   723.39   725.48   721.85
Naive implementation (insertion sort)      503.67   174.09   182.13   284.41   191.10
Insertion Sort (Daniel Stutzbach)          345.32   152.84   157.67   151.23   150.96
Insertion Sort Unrolled                    316.20   133.03   129.86   118.96   105.06
Rank Order                                 164.37   138.32   46.29    99.87    99.81
Rank Order with registers                  115.44   116.02   44.04    116.04   116.03
Sorting Networks (Daniel Stutzbach)        230.35   114.31   119.15   110.51   111.45
Sorting Networks (Paul R)                  498.94   77.24    63.98    62.17    65.67
Sorting Networks 12 with Fast Swap         315.98   59.41    58.36    60.29    55.15
Sorting Networks 12 reordered Swap         307.67   55.78    51.48    51.67    50.74
Reordered Sorting Network w/ fast swap     149.68   31.46    30.91    31.54    31.58

Sandy Bridge (i7-2600k)
=======================
Direct call to qsort library function      559.97   451.88   464.84   491.35   458.11
Naive implementation (insertion sort)      341.15   160.26   160.45   154.40   106.54
Insertion Sort (Daniel Stutzbach)          284.17   136.74   132.69   123.85   121.77
Insertion Sort Unrolled                    239.40   110.49   114.81   110.79   117.30
Rank Order                                 114.24   76.42    45.31    36.96    36.73
Rank Order with registers                  105.09   32.31    48.54    32.51    33.29
Sorting Networks (Daniel Stutzbach)        210.56   115.68   116.69   107.05   124.08
Sorting Networks (Paul R)                  364.03   66.02    61.64    45.70    44.19
Sorting Networks 12 with Fast Swap         246.97   41.36    59.03    41.66    38.98
Sorting Networks 12 reordered Swap         235.39   38.84    47.36    38.61    37.29
Reordered Sorting Network w/ fast swap     115.58   27.23    27.75    27.25    26.54

Nehalem (Xeon E5640)
====================
Direct call to qsort library function      911.62   890.88   681.80   876.03   872.89
Naive implementation (insertion sort)      457.69   236.87   127.68   388.74   175.28
Insertion Sort (Daniel Stutzbach)          317.89   279.74   147.78   247.97   245.09
Insertion Sort Unrolled                    259.63   220.60   116.55   221.66   212.93
Rank Order                                 140.62   197.04   52.10    163.66   153.63
Rank Order with registers                  84.83    96.78    50.93    109.96   54.73
Sorting Networks (Daniel Stutzbach)        214.59   220.94   118.68   120.60   116.09
Sorting Networks (Paul R)                  459.17   163.76   56.40    61.83    58.69
Sorting Networks 12 with Fast Swap         284.58   95.01    50.66    53.19    55.47
Sorting Networks 12 reordered Swap         281.20   96.72    44.15    56.38    54.57
Reordered Sorting Network w/ fast swap     128.34   50.87    26.87    27.91    28.02

レジスタ変数のアイデアは、レックスカーの「ランクオーダー」ソリューションに適用する必要があります。これが最も速く、おそらく-O3最適化は逆効果にならないでしょう。
cdunn2001

1
@ cdunn2001私はそれをテストしましたが、改善は見られません(-O0と-Osでの数サイクルを除く)。asmを見ると、gccはすでにレジスタを使用してmemcpyへの呼び出しを排除することに成功しているようです。
ケビン株

単純なスワップバージョンをテストスイートに追加してもよろしいですか。手動で最適化されたアセンブリの高速スワップと比較すると興味深いでしょう。
クリス

1
あなたのコードはまだGundersonのswapを使用しています#define SWAP(x,y) { int oldx = x; x = x < y ? x : y; y ^= oldx ^ x; }
Paolo Bonzini、2011

@Paolo Bonzini:はい、私はあなたのテストケースを追加するつもりですが、まだ時間がありません。ただし、インラインアセンブリは避けます。
クリス

15

数日前にGoogleからこの質問に出くわしました。これは、6つの整数の固定長配列をすばやくソートする必要もあったためです。ただし、私の場合、整数は8ビット(32ビットではなく)のみであり、Cのみを使用するという厳密な要件はありません。誰かの役に立つかもしれない場合は、とにかく私の発見を共有すると思いました...

SSEを使用して、可能な範囲で比較およびスワップ操作をベクトル化する、ネットワークソートのバリアントをアセンブリに実装しました。配列を完全にソートするには、6つの「パス」が必要です。新しいメカニズムを使用して、PCMPGTB(ベクトル化された比較)の結果を直接PSHUFB(ベクトル化されたスワップ)のシャッフルパラメーターに変換し、PADDB(ベクトル化された加算)と場合によってはPAND(ビットごとのAND)命令も使用しました。

このアプローチには、真に分岐のない関数を生成するという副作用もありました。ジャンプの指示は一切ありません。

この実装、質問で最も高速なオプションとして現在マークされている実装(「単純なスワップによるネットワーク12のソーティング」)より約38%速いようです。charテスト中に配列要素を使用するようにその実装を変更し、比較を公平にしました。

このアプローチは、最大16要素の任意の配列サイズに適用できることに注意してください。アレイが大きくなるほど、他の方法よりも相対的に速度が有利になることを期待しています。

コードは、SSSE3を搭載したx86_64プロセッサー用のMASMで記述されています。この関数は、「新しい」Windows x64呼び出し規約を使用します。ここにあります...

PUBLIC simd_sort_6

.DATA

ALIGN 16

pass1_shuffle   OWORD   0F0E0D0C0B0A09080706040503010200h
pass1_add       OWORD   0F0E0D0C0B0A09080706050503020200h
pass2_shuffle   OWORD   0F0E0D0C0B0A09080706030405000102h
pass2_and       OWORD   00000000000000000000FE00FEFE00FEh
pass2_add       OWORD   0F0E0D0C0B0A09080706050405020102h
pass3_shuffle   OWORD   0F0E0D0C0B0A09080706020304050001h
pass3_and       OWORD   00000000000000000000FDFFFFFDFFFFh
pass3_add       OWORD   0F0E0D0C0B0A09080706050404050101h
pass4_shuffle   OWORD   0F0E0D0C0B0A09080706050100020403h
pass4_and       OWORD   0000000000000000000000FDFD00FDFDh
pass4_add       OWORD   0F0E0D0C0B0A09080706050403020403h
pass5_shuffle   OWORD   0F0E0D0C0B0A09080706050201040300h
pass5_and       OWORD 0000000000000000000000FEFEFEFE00h
pass5_add       OWORD   0F0E0D0C0B0A09080706050403040300h
pass6_shuffle   OWORD   0F0E0D0C0B0A09080706050402030100h
pass6_add       OWORD   0F0E0D0C0B0A09080706050403030100h

.CODE

simd_sort_6 PROC FRAME

    .endprolog

    ; pxor xmm4, xmm4
    ; pinsrd xmm4, dword ptr [rcx], 0
    ; pinsrb xmm4, byte ptr [rcx + 4], 4
    ; pinsrb xmm4, byte ptr [rcx + 5], 5
    ; The benchmarked 38% faster mentioned in the text was with the above slower sequence that tied up the shuffle port longer.  Same on extract
    ; avoiding pins/extrb also means we don't need SSE 4.1, but SSSE3 CPUs without SSE4.1 (e.g. Conroe/Merom) have slow pshufb.
    movd    xmm4, dword ptr [rcx]
    pinsrw  xmm4,  word ptr [rcx + 4], 2  ; word 2 = bytes 4 and 5


    movdqa xmm5, xmm4
    pshufb xmm5, oword ptr [pass1_shuffle]
    pcmpgtb xmm5, xmm4
    paddb xmm5, oword ptr [pass1_add]
    pshufb xmm4, xmm5

    movdqa xmm5, xmm4
    pshufb xmm5, oword ptr [pass2_shuffle]
    pcmpgtb xmm5, xmm4
    pand xmm5, oword ptr [pass2_and]
    paddb xmm5, oword ptr [pass2_add]
    pshufb xmm4, xmm5

    movdqa xmm5, xmm4
    pshufb xmm5, oword ptr [pass3_shuffle]
    pcmpgtb xmm5, xmm4
    pand xmm5, oword ptr [pass3_and]
    paddb xmm5, oword ptr [pass3_add]
    pshufb xmm4, xmm5

    movdqa xmm5, xmm4
    pshufb xmm5, oword ptr [pass4_shuffle]
    pcmpgtb xmm5, xmm4
    pand xmm5, oword ptr [pass4_and]
    paddb xmm5, oword ptr [pass4_add]
    pshufb xmm4, xmm5

    movdqa xmm5, xmm4
    pshufb xmm5, oword ptr [pass5_shuffle]
    pcmpgtb xmm5, xmm4
    pand xmm5, oword ptr [pass5_and]
    paddb xmm5, oword ptr [pass5_add]
    pshufb xmm4, xmm5

    movdqa xmm5, xmm4
    pshufb xmm5, oword ptr [pass6_shuffle]
    pcmpgtb xmm5, xmm4
    paddb xmm5, oword ptr [pass6_add]
    pshufb xmm4, xmm5

    ;pextrd dword ptr [rcx], xmm4, 0    ; benchmarked with this
    ;pextrb byte ptr [rcx + 4], xmm4, 4 ; slower version
    ;pextrb byte ptr [rcx + 5], xmm4, 5
    movd   dword ptr [rcx], xmm4
    pextrw  word ptr [rcx + 4], xmm4, 2  ; x86 is little-endian, so this is the right order

    ret

simd_sort_6 ENDP

END

これを実行可能オブジェクトにコンパイルして、Cプロジェクトにリンクできます。Visual Studioでこれを行う方法については、この記事を参照してください。次のCプロトタイプを使用して、Cコードから関数を呼び出すことができます。

void simd_sort_6(char *values);

あなたの提案を他の議会レベルの提案と比較するのは興味深いことです。実装の比較パフォーマンスにはそれらは含まれていません。とにかく、SSEを使用するのは良いことです。
クリス

今後の研究のもう1つの分野は、この問題への新しいインテルAVX命令の適用です。より大きな256ビットのベクトルは、8つのDWORDに適合するのに十分な大きさです。
Joe Crivello

1
の代わりにpxor / pinsrd xmm4, mem, 0movd
Peter Cordes

14

テストコードはかなり悪いです。それは初期配列をオーバーフローします(ここで人々はコンパイラの警告を読みませんか?)、printfは間違った要素を出力します。正当な理由がないため、rdtscに.byteを使用します。実行は1つしかありません(!)。最終結果は実際には正しいです(そのため、微妙に間違ったものに「最適化」するのは非常に簡単です)、含まれているテストは非常に初歩的で(負の数はありませんか?)、コンパイラが関数全体をデッドコードとして単に破棄するのを止めるものはありません。

そうは言っても、bitonicネットワークソリューションを改善するのは非常に簡単です。単純に最小/最大/スワップを変更する

#define SWAP(x,y) { int tmp; asm("mov %0, %2 ; cmp %1, %0 ; cmovg %1, %0 ; cmovg %2, %1" : "=r" (d[x]), "=r" (d[y]), "=r" (tmp) : "0" (d[x]), "1" (d[y]) : "cc"); }

そして、それは私にとって約65%速く出てきます(-O2、amd64、Core i7を使用したDebian gcc 4.4.5)。


OK、テストコードは貧弱です。自由に改善してください。はい、アセンブリコードを使用できます。どうしてx86アセンブラを使用して完全にコーディングしないのですか?それは少しポータブルではないかもしれませんが、なぜわざわざ?
クリス、

配列のオーバーフローに気付いてくれてありがとう、それを修正しました。オーバーフローのないコードのコピー/貼り付けへのリンクをクリックしたため、他の人はそれに気付かなかったかもしれません。
クリス

4
実際にはアセンブラも必要ありません。すべての巧妙なトリックをドロップするだけで、GCCはシーケンスを認識し、条件付きの動きを挿入します。#define min(a、b)((a <b)?a:b)#define max(a、b)( (a <b)?b:a)#define SWAP(x、y){int a = min(d [x]、d [y]); int b = max(d [x]、d [y]); d [x] = a; d [y] = b; }おそらくインラインasmバリアントよりも数パーセント遅くなりますが、適切なベンチマークの欠如を考えると、それを言うのは難しいです。
Steinar H. Gunderson、2011

3
…そして最後に、数値が浮動小数点数であり、NaNなどを心配する必要がない場合、GCCはこれをminss / maxss SSE命令に変換できますが、これはさらに約25%高速です。士気:巧妙なビットフィドルトリックを削除し、コンパイラーにその仕事を任せます。:-)
シュタイナーH.ガンダーソン2011

13

提供されているスワップマクロは本当に気に入っていますが、

#define min(x, y) (y ^ ((x ^ y) & -(x < y)))
#define max(x, y) (x ^ ((x ^ y) & -(x < y)))
#define SWAP(x,y) { int tmp = min(d[x], d[y]); d[y] = max(d[x], d[y]); d[x] = tmp; }

改善が見られます(これは優れたコンパイラーによって実現される可能性があります)。

#define SWAP(x,y) { int tmp = ((x ^ y) & -(y < x)); y ^= tmp; x ^= tmp; }

minとmaxがどのように機能するかに注意し、共通の部分式を明示的に引き出します。これにより、minおよびmaxマクロが完全に削除されます。


それはそれらを逆方向に取得し、d [y]が最大値、つまりx ^(共通部分式)を取得することに注意してください。
ケビン株

私も同じことに気づきました。私はあなたの実装が正しいのではd[x]なくx(と同じy)、そしてd[y] < d[x]ここでの不平等のために(そうです、最小/最大コードとは異なり)正しいと思います。
タイラー

私はあなたのスワップで試しましたが、ローカル最適化はより大きなレベルで悪影響を及ぼします(依存関係を導入すると思います)。そして、結果は他のスワップよりも遅くなります。しかし、提案された新しいソリューションでわかるように、スワップを最適化することで得られるパフォーマンスは確かに多くありました。
クリス

12

ベンチマークを行ったり、実際のコンパイラによって生成されたアセンブリを見たりせずに、最小/最大を最適化しないでください。条件付きの移動命令でGCCにminを最適化させると、33%高速化されます。

#define SWAP(x,y) { int dx = d[x], dy = d[y], tmp; tmp = d[x] = dx < dy ? dx : dy; d[y] ^= dx ^ tmp; }

(テストコードでは280対420サイクル)。?でmaxを実行することはほぼ同じですが、ほとんどノイズで失われますが、上記は少し高速です。このSWAPは、GCCとClangの両方でより高速です。

コンパイラーはまた、レジスター割り当てとエイリアス分析で並外れた仕事をしており、d [x]をローカル変数に効率的に移動し、最後にメモリにコピーし直すだけです。実際、ローカル変数を完全に操作する場合(たとえば、d0 = d[0], d1 = d[1], d2 = d[2], d3 = d[3], d4 = d[4], d5 = d[5])を。私はこれを書いているのは、強力な最適化を想定しているにもかかわらず、コンパイラーを最小/最大よりも上回ろうとしているためです。:)

ちなみにClangとGCCを試しました。これらは同じ最適化を行いますが、スケジュールの違いにより、2つは結果に多少のばらつきがあり、どちらが速いか遅いかは実際には言えません。GCCはソートネットワークでより高速で、Clangは二次ソートで高速です。

完全を期すために、展開されたバブルソートと挿入ソートも可能です。これがバブルソートです。

SWAP(0,1); SWAP(1,2); SWAP(2,3); SWAP(3,4); SWAP(4,5);
SWAP(0,1); SWAP(1,2); SWAP(2,3); SWAP(3,4);
SWAP(0,1); SWAP(1,2); SWAP(2,3);
SWAP(0,1); SWAP(1,2);
SWAP(0,1);

そしてここに挿入ソートがあります:

//#define ITER(x) { if (t < d[x]) { d[x+1] = d[x]; d[x] = t; } }
//Faster on x86, probably slower on ARM or similar:
#define ITER(x) { d[x+1] ^= t < d[x] ? d[x] ^ d[x+1] : 0; d[x] = t < d[x] ? t : d[x]; }
static inline void sort6_insertion_sort_unrolled_v2(int * d){
    int t;
    t = d[1]; ITER(0);
    t = d[2]; ITER(1); ITER(0);
    t = d[3]; ITER(2); ITER(1); ITER(0);
    t = d[4]; ITER(3); ITER(2); ITER(1); ITER(0);
    t = d[5]; ITER(4); ITER(3); ITER(2); ITER(1); ITER(0);

この挿入の並べ替えは、Daniel Stutzbachのものよりも高速で、GPUまたはプレディケーションを備えたコンピューターで特に優れています。これは、ITERが3つの命令(SWAPでは4つ)で実行できるためです。たとえば、以下はt = d[2]; ITER(1); ITER(0);ARMアセンブリの行です。

    MOV    r6, r2
    CMP    r6, r1
    MOVLT  r2, r1
    MOVLT  r1, r6
    CMP    r6, r0
    MOVLT  r1, r0
    MOVLT  r0, r6

6つの要素について、挿入ソートはソートネットワークと競合します(12スワップvs 15反復は4命令/スワップvs 3命令/反復のバランスをとります)。もちろん、バブルのソートは遅いです。ただし、挿入ソートはO(n ^ 2)であり、ソートネットワークはO(n log n)であるため、サイズが大きくなると正しくありません。


1
多かれ少なかれ関連:コンパイラで直接最適化を実装できるように、GCCにレポートを提出しました。それが行われるかどうかはわかりませんが、少なくともそれがどのように進化するかを追跡できます。
Morwenn、2015年

11

私はテストスイートを識別できないPPCアーキテクチャマシンに移植しました(コードに手を加える必要はありませんでした。テストの反復を増やし、8つのテストケースを使用して結果をmodで汚染しないようにし、x86固有のrdtscを置き換えます)。

qsortライブラリ関数の直接呼び出し:101

単純な実装(挿入ソート):299

挿入ソート(Daniel Stutzbach) :108

挿入ソートアンロール :51

ソーティングネットワーク(Daniel Stutzbach) :26

ソーティングネットワーク(Paul R) :85

高速スワップによるネットワーク12のソート :117

ソーティングネットワーク12再注文スワップ :116

ランク順 :56


1
本当に面白いです。PPCでは、ブランチレススワップは悪い考えのようです。また、コンパイラ関連の影響である可能性もあります。どちらを使用しましたか?
KRISS

そのgccコンパイラのブランチ-最小、最大のロジックはおそらくブランチレスではありません-逆アセンブルを検査して通知しますが、コンパイラがx <yのようなものを含めて十分に賢い場合を除き、ifはブランチになります-x86 / x64 CMOV命令はこれを回避する可能性がありますが、PPCの固定小数点値に対するそのような命令はなく、浮動小数点のみです。私は明日これに手を出してあなたに知らせるかもしれません-Winamp AVSソースにははるかに単純なブランチレスのmin / maxがあったことを覚えていますが、それはフロートのみのためでしたが、本当にブランチレスのアプローチへの良いスタートかもしれません。
jheriko '25

4
以下は、符号なし入力のあるPPCのブランチレス最小/最大ですsubfc r5,r4,r3; subfe r6,r6,r6; andc r6,r5,r6; add r4,r6,r4; subf r3,r6,r3。r3 / r4は入力、r5 / r6はスクラッチレジスタ、出力ではr3が最小値を取得し、r4が最大値を取得します。手作業できちんとスケジュールできる必要があります。私は、GNUスーパーオプティマイザーを使用して、4つの命令の最小シーケンスと最大シーケンスから始めて、組み合わせることができる2つを手動で探しました。符号付き入力の場合は、もちろん、最初にすべての要素に0x80000000を追加し、最後に再度減算して、符号なしの場合と同じように処理できます。
Paolo Bonzini、2011

7

XORスワップは、スワッピング関数で役立つ場合があります。

void xorSwap (int *x, int *y) {
     if (*x != *y) {
         *x ^= *y;
         *y ^= *x;
         *x ^= *y;
     }
 }

ifはコードの相違を大きくしすぎる可能性がありますが、すべてのintが一意であることが保証されている場合、これは便利です。


1
xor swapは同じ値でも動作します... x ^ = yはxを0に設定し、y ^ = xはyをyのままにします(== x)、x ^ = yはxをyに設定します
jheriko

11
うまくいかないときと、同じ場所xy指すときです。
ホブ、2011

とにかく、ソーティングネットワークで使用する場合、xとyの両方が同じ場所を指すことはありません。ブランチレススワップと同じ効果を得るには、テストを回避する方法を見つける方法がまだあります。それを達成するためのアイデアがあります。
クリス、

5

これを試してみて、これらの例から学ぶことを楽しみにしていますが、最初に、1.5 GHz PPC Powerbook G4(1 GB DDR RAM搭載)からのいくつかのタイミング。(私はhttp://www.mcs.anl.gov/~kazutomo/rdtsc.htmlからPPCの同様のrdtscのようなタイマーを借りましたタイミングのために、。)私はプログラムを数回実行し、絶対的な結果はさまざまでしたが、一貫して最速のテストは「挿入ソート(Daniel Stutzbach)」で、「挿入ソートが展開されました」が2番手でした。

これが最後の時間です。

**Direct call to qsort library function** : 164
**Naive implementation (insertion sort)** : 138
**Insertion Sort (Daniel Stutzbach)**     : 85
**Insertion Sort Unrolled**               : 97
**Sorting Networks (Daniel Stutzbach)**   : 457
**Sorting Networks (Paul R)**             : 179
**Sorting Networks 12 with Fast Swap**    : 238
**Sorting Networks 12 reordered Swap**    : 236
**Rank Order**                            : 116

4

これがこのスレッドへの私の貢献です:一意の値を含む6メンバーの整数ベクトル(valp)用に最適化された1、4ギャップのシェルソート。

void shellsort (int *valp)
{      
  int c,a,*cp,*ip=valp,*ep=valp+5;

  c=*valp;    a=*(valp+4);if (c>a) {*valp=    a;*(valp+4)=c;}
  c=*(valp+1);a=*(valp+5);if (c>a) {*(valp+1)=a;*(valp+5)=c;}

  cp=ip;    
  do
  {
    c=*cp;
    a=*(cp+1);
    do
    {
      if (c<a) break;

      *cp=a;
      *(cp+1)=c;
      cp-=1;
      c=*cp;
    } while (cp>=valp);
    ip+=1;
    cp=ip;
  } while (ip<ep);
}

デュアルコアAthlon M300 @ 2 Ghz(DDR2メモリ)を搭載したHP dv7-3010soラップトップでは、165クロックサイクルで実行されます。これは、すべての一意のシーケンスのタイミングから計算された平均です(合計6!/ 720)。OpenWatcom 1.8を使用してWin32にコンパイルされています。ループは本質的に挿入ソートであり、16命令/ 37バイト長です。

コンパイルする64ビット環境がありません。


いいね。私はそれをより長いテスト
スイートに

3

ここで挿入ソートがかなり競争力がある場合は、シェルソートを試すことをお勧めします。恐らく、6つの要素はおそらく最高にするには少なすぎますが、試してみる価値があるかもしれません。

コード例、テストされていない、デバッグされていない、など。inc= 4およびinc-= 3シーケンスを調整して、最適なものを見つけます(たとえば、inc = 2、inc-= 1を試してください)。

static __inline__ int sort6(int * d) {
    char j, i;
    int tmp;
    for (inc = 4; inc > 0; inc -= 3) {
        for (i = inc; i < 5; i++) {
            tmp = a[i];
            j = i;
            while (j >= inc && a[j - inc] > tmp) {
                a[j] = a[j - inc];
                j -= inc;
            }
            a[j] = tmp;
        }
    }
}

私はこれが勝つとは思いませんが、誰かが10個の要素のソートについて質問を投稿した場合、誰が知っていますか...

ウィキペディアによると、これはソーティングネットワークと組み合わせることもできます: Pratt、V(1979)。Shellsortとソーティングネットワーク(コンピューターサイエンスの優れた論文)。花輪。ISBN 0-824-04406-1


いくつかの実装をご自由に提案してください:-)
kriss

提案を追加しました。バグをお楽しみください。
gcp

3

私はスーパーレイトですが、いくつかの異なるソリューションを試すことに興味がありました。最初に、私はそのペーストをクリーンアップし、コンパイルして、リポジトリーに入れました。他の人が試さないように、私はいくつかの望ましくない解決策を行き止まりとして残しました。この中には、x1> x2が1回計算されることを確認するための最初の解決策がありました。最適化後は、他の単純なバージョンよりも速くなりません。

この研究の私自身のアプリケーションは2〜8のアイテムをソートするためのものなので、ランク付けソートのループバージョンを追加しました。可変数の引数があるため、ループが必要です。これが、ソーティングネットワークソリューションを無視した理由でもあります。

テストコードは重複が正しく処理されたことをテストしていなかったため、既存のソリューションはすべて正しく、テストコードに特別なケースを追加して、重複が正しく処理されたことを確認しました。

次に、完全にAVXレジスターにある挿入ソートを作成しました。私のマシンでは、他の挿入ソートより25%高速ですが、ランクオーダーより100%遅いです。私は純粋に実験のためにこれを行いましたが、挿入ソートの分岐のためにこれがより良いとは期待していませんでした。

static inline void sort6_insertion_sort_avx(int* d) {
    __m256i src = _mm256_setr_epi32(d[0], d[1], d[2], d[3], d[4], d[5], 0, 0);
    __m256i index = _mm256_setr_epi32(0, 1, 2, 3, 4, 5, 6, 7);
    __m256i shlpermute = _mm256_setr_epi32(7, 0, 1, 2, 3, 4, 5, 6);
    __m256i sorted = _mm256_setr_epi32(d[0], INT_MAX, INT_MAX, INT_MAX,
            INT_MAX, INT_MAX, INT_MAX, INT_MAX);
    __m256i val, gt, permute;
    unsigned j;
     // 8 / 32 = 2^-2
#define ITER(I) \
        val = _mm256_permutevar8x32_epi32(src, _mm256_set1_epi32(I));\
        gt =  _mm256_cmpgt_epi32(sorted, val);\
        permute =  _mm256_blendv_epi8(index, shlpermute, gt);\
        j = ffs( _mm256_movemask_epi8(gt)) >> 2;\
        sorted = _mm256_blendv_epi8(_mm256_permutevar8x32_epi32(sorted, permute),\
                val, _mm256_cmpeq_epi32(index, _mm256_set1_epi32(j)))
    ITER(1);
    ITER(2);
    ITER(3);
    ITER(4);
    ITER(5);
    int x[8];
    _mm256_storeu_si256((__m256i*)x, sorted);
    d[0] = x[0]; d[1] = x[1]; d[2] = x[2]; d[3] = x[3]; d[4] = x[4]; d[5] = x[5];
#undef ITER
}

次に、AVXを使用してランク順ソートを作成しました。これは他の順位ソリューションの速度と一致しますが、それより速くはありません。ここでの問題は、AVXでのみインデックスを計算できることです。次に、インデックスのテーブルを作成する必要があります。これは、計算がソースベースではなく宛先ベースであるためです。ソースベースのインデックスから宛先ベースのインデックスへの変換を参照してください。

static inline void sort6_rank_order_avx(int* d) {
    __m256i ror = _mm256_setr_epi32(5, 0, 1, 2, 3, 4, 6, 7);
    __m256i one = _mm256_set1_epi32(1);
    __m256i src = _mm256_setr_epi32(d[0], d[1], d[2], d[3], d[4], d[5], INT_MAX, INT_MAX);
    __m256i rot = src;
    __m256i index = _mm256_setzero_si256();
    __m256i gt, permute;
    __m256i shl = _mm256_setr_epi32(1, 2, 3, 4, 5, 6, 6, 6);
    __m256i dstIx = _mm256_setr_epi32(0,1,2,3,4,5,6,7);
    __m256i srcIx = dstIx;
    __m256i eq = one;
    __m256i rotIx = _mm256_setzero_si256();
#define INC(I)\
    rot = _mm256_permutevar8x32_epi32(rot, ror);\
    gt = _mm256_cmpgt_epi32(src, rot);\
    index = _mm256_add_epi32(index, _mm256_and_si256(gt, one));\
    index = _mm256_add_epi32(index, _mm256_and_si256(eq,\
                _mm256_cmpeq_epi32(src, rot)));\
    eq = _mm256_insert_epi32(eq, 0, I)
    INC(0);
    INC(1);
    INC(2);
    INC(3);
    INC(4);
    int e[6];
    e[0] = d[0]; e[1] = d[1]; e[2] = d[2]; e[3] = d[3]; e[4] = d[4]; e[5] = d[5];
    int i[8];
    _mm256_storeu_si256((__m256i*)i, index);
    d[i[0]] = e[0]; d[i[1]] = e[1]; d[i[2]] = e[2]; d[i[3]] = e[3]; d[i[4]] = e[4]; d[i[5]] = e[5];
}

リポジトリはここにあります:https : //github.com/eyepatchParrot/sort6/


1
あなたは使用することができますvmovmskps右シフトにbitscan(必要性を回避すること、(幸せな組み込み関数を維持するためのキャストとの)整数ベクトル上ffs)の結果を。
Peter Cordes

1
でマスクするのではなく、減算するcmpgtことにより、結果に基づいて条件付きで1を追加できます。例えばませんset1(1)index = _mm256_sub_epi32(index, gt)index -= -1 or 0;
ピーター・コルド

1
eq = _mm256_insert_epi32(eq, 0, I)記述どおりにコンパイルする場合、要素をゼロにする効率的な方法ではありません(特に、低位4以外の要素の場合vpinsrd、XMM宛先でのみ使用できるため、3より大きいインデックスをエミュレートする必要があります)。代わりに、ゼロ化されたベクトルを使用した_mm256_blend_epi32vpblendd)。 vpblenddIntel CPUでポート5を必要とするシャッフルに対して、任意のポートで実行されるシングルUOP命令です。(agner.org/optimize)。
Peter Cordes

1
また、rot同じソースから異なるシャッフルでベクトルを生成するか、レーン交差シャッフル(3サイクルのレイテンシー)を介して単一のdepチェーンを実行する代わりに、交互に使用する2つのdepチェーンを並行して実行することを検討することもできます。これにより、1つのソート内でILPが増加します。2つのdepチェインは、ベクトル定数の数を適切な数に制限します。2つは、1つの回転に対して1つ、2つの回転ステップを組み合わせた場合に1つです。
Peter Cordes

2

この質問はかなり古くなっていますが、実際には最近、同じ問題を解決する必要がありました。小さな配列をソートするための高速なアゴリズムです。私の知識を共有することは良い考えだと思いました。私は最初にソートネットワークを使用して開始しましたが、6つの値のすべての順列をソートするために実行された比較の合計数がソートネットワークよりも少なく、挿入ソートよりも小さい他のアルゴリズムを見つけることができました。スワップの数は数えませんでした。私はそれがほぼ同等であると期待します(たぶん少し高いかもしれません)。

アルゴリズムsort6は、アルゴリズムsort4を使用するアルゴリズムを使用しますsort3。これは、いくつかの軽いC ++フォームでの実装です(元のテンプレートは重いので、ランダムアクセスイテレータや適切な比較関数で動作します)。

3つの値の並べ替え

次のアルゴリズムは、展開された挿入ソートです。2つのスワップ(6つの割り当て)を実行する必要がある場合、代わりに4つの割り当てを使用します。

void sort3(int* array)
{
    if (array[1] < array[0]) {
        if (array[2] < array[0]) {
            if (array[2] < array[1]) {
                std::swap(array[0], array[2]);
            } else {
                int tmp = array[0];
                array[0] = array[1];
                array[1] = array[2];
                array[2] = tmp;
            }
        } else {
            std::swap(array[0], array[1]);
        }
    } else {
        if (array[2] < array[1]) {
            if (array[2] < array[0]) {
                int tmp = array[2];
                array[2] = array[1];
                array[1] = array[0];
                array[0] = tmp;
            } else {
                std::swap(array[1], array[2]);
            }
        }
    }
}

2〜3の比較と最大4つの割り当てを使用して3つの値をソートすることで、配列の可能なすべての置換に対して、ソートは多かれ少なかれ1つのブランチを持っているため、少し複雑に見えます。

4つの値の並べ替え

これはsort3次に呼び出して、配列の最後の要素で展開された挿入ソートを実行します。

void sort4(int* array)
{
    // Sort the first 3 elements
    sort3(array);

    // Insert the 4th element with insertion sort 
    if (array[3] < array[2]) {
        std::swap(array[2], array[3]);
        if (array[2] < array[1]) {
            std::swap(array[1], array[2]);
            if (array[1] < array[0]) {
                std::swap(array[0], array[1]);
            }
        }
    }
}

このアルゴリズムは、3〜6回の比較と最大5回のスワップを実行します。挿入ソートの展開は簡単ですが、最後のソートには別のアルゴリズムを使用します...

6つの値の並べ替え

これは、私が二重挿入ソートと呼んだもののアンロールバージョンを使用しています。名前はそれほどすばらしいものではありませんが、非常にわかりやすくなっています。以下にその仕組みを示します。

  • 配列の最初と最後の要素を除くすべてをソートします。
  • 最初が最後よりも大きい場合は、最初と配列の要素を入れ替えます。
  • 並べ替えられたシーケンスに最初から最初の要素を挿入し、最後から最後の要素を挿入します。

スワップ後、最初の要素は常に最後の要素よりも小さくなります。つまり、ソートされたシーケンスにそれらを挿入する場合、最悪の場合に2つの要素を挿入するための比較はN回以下になります。たとえば、最初の要素が3番目の位置に挿入されている場合、最後の要素は4番目の位置より下に挿入できません。

void sort6(int* array)
{
    // Sort everything but first and last elements
    sort4(array+1);

    // Switch first and last elements if needed
    if (array[5] < array[0]) {
        std::swap(array[0], array[5]);
    }

    // Insert first element from the front
    if (array[1] < array[0]) {
        std::swap(array[0], array[1]);
        if (array[2] < array[1]) {
            std::swap(array[1], array[2]);
            if (array[3] < array[2]) {
                std::swap(array[2], array[3]);
                if (array[4] < array[3]) {
                    std::swap(array[3], array[4]);
                }
            }
        }
    }

    // Insert last element from the back
    if (array[5] < array[4]) {
        std::swap(array[4], array[5]);
        if (array[4] < array[3]) {
            std::swap(array[3], array[4]);
            if (array[3] < array[2]) {
                std::swap(array[2], array[3]);
                if (array[2] < array[1]) {
                    std::swap(array[1], array[2]);
                }
            }
        }
    }
}

6つの値のすべての順列に対する私のテストは、このアルゴリズムが常に6〜13の比較を実行することを示しています。実行されたスワップの数は計算しませんでしたが、最悪の場合11を超えるとは思いません。

この質問がもう実際の問題を表していない場合でも、これが役立つことを願っています:)

編集:それを提供されたベンチマークに入れた後、興味深い代替手段のほとんどより明らかに遅いです。展開された挿入ソートより少しパフォーマンスが良い傾向がありますが、それだけです。基本的に、これは整数の場合は最適なソートではありませんが、高価な比較演算を伴う型には興味深いものになる可能性があります。


これらはいいです。解決された問題は何十年も前のものであり、おそらくCプログラミングと同じくらい古いので、この問題は現在5年近くあるため、あまり関連性がないように見えます。
クリス

あなたは他の答えが時間を計られる方法を見るべきです。重要なのは、このような小さなデータセットでは、比較、さらには比較とスワップのカウントでは、アルゴリズムの速さは実際には言われないということです(基本的に、6つのintのソートは常にO(1)です(O(6 * 6)はO(1)であるため))。以前に提案されたソリューションの現在最速の方法は、(RexKerrによる)大きな比較を使用して各値の位置をすぐに見つけることです。
kriss '10 / 07/15

@kriss最速ですか?結果を読んだところ、ソーティングネットワークアプローチが最速でした。また、私の解決策は私の汎用ライブラリからのものであり、常に整数を比較しているわけではなく、常に比較に使用operator<しているわけでもありません。比較とスワップの客観的なカウントに加えて、アルゴリズムのタイミングも適切に設定しました。この解決策は最も一般的な解決策でしたが、確かに@RexKerrの解決策はありませんでした。それを試してみるつもりです:)
Morwenn

RexKerr(注文ランク)によるソリューションは、gccコンパイラ4.2.3以降(およびgcc 4.9以降では2番目に優れたもののほぼ2倍になった)、X86アーキテクチャで最速になりました。しかし、これはコンパイラの最適化に大きく依存しており、他のアーキテクチャでは当てはまらない場合があります。
クリス、2015年

@kriss知っておくと面白いですね。そして、私は確かに再びとの違いを増やすことができました-O3。私は私の並べ替えライブラリに別の戦略を採用すると思います:比較の数が少ない、スワップの数が少ない、または潜在的に最高のパフォーマンスを実現する3種類のアルゴリズムを提供します。少なくとも、何が起こるかは読者にとって透過的です。あなたの洞察をありがとう:)
Morwenn

1

質問には2つの部分があると思います。

  • 1つ目は、最適なアルゴリズムを決定することです。これは、少なくともこの場合は、比較とスワップの正確な最小、最大、平均、および標準偏差を計算できるすべての可能な順序(それほど多くはありません)をループすることによって行われます。準優勝者も2〜3人います。
  • 2つ目は、アルゴリズムを最適化することです。教科書のコード例を意味のある現実的なアルゴリズムに変換するために多くのことができます。アルゴリズムを必要な範囲まで最適化できないことがわかった場合は、次点を試してください。

パイプラインを空にすることについてあまり心配しません(現在のx86を想定):分岐予測は長い道のりを歩んできました。私が心配するのは、コードとデータがそれぞれ1つのキャッシュライン(コードでは2つ)に収まるようにすることです。フェッチのレイテンシが低下すると、ストールが解消されます。また、内部ループはおそらく10命令程度になることも意味します(私の並べ替えアルゴリズムには2つの異なる内部ループがあり、それぞれ10命令/ 22バイトと9/22長です)。コードにdivが含まれていないとすれば、それは盲目的に高速になるでしょう。


あなたの答えを理解する方法がわかりません。まず、私はあなたが提案しているアルゴリズムがまったくわかりませんか?そして、720の可能な順序をループする必要がある場合にどのように最適であるか(既存の回答は720サイクルよりはるかに少ない)。ランダムな入力がある場合、(理論的なレベルであっても)すべての入力データを考慮しない場合を除いて、分岐予測が50〜50よりも優れたパフォーマンスを発揮することは想像できません。また、すでに提案されているほとんどの優れたソリューションは、データとコードの両方を完全にキャッシュで処理する可能性が高いです。しかし、多分私はあなたの答えを完全に誤解した。いくつかのコードを示しているのですか?
クリス

私が意味したのは、6つの整数の組み合わせは720(6!)しかなく、それらすべてを候補アルゴリズムに通すことで、前述のように多くのことを決定できるということでした。これが理論的な部分です。実用的な部分は、可能な限り少ないクロックサイクルで実行するようにアルゴリズムを微調整することです。6つの整数をソートするための私の出発点は、1、4ギャップのシェルソートです。4つのギャップは、1つのギャップでの分岐予測を正しく行うための道を開きます。
Olof Forshell、2012年

6の1、4ギャップシェルソート!ユニークな組み合わせ(0112345で始まり543210で終わる)は、7つの比較と0の交換の最良のケースと、14の比較と10の交換の最悪のケースになります。平均的なケースは約11.14の比較と6つの交換です。
Olof Forshell

1
「正規のランダム分布」が得られません-私がしていることは、可能なすべての組み合わせをテストし、最小/平均/最大統計を決定することです。Shellsortは、増加する増分の一連の挿入ソートで、最終的な増分-1-は、純粋な挿入ソートのように単独で実行する場合よりもはるかに少ない作業です。クロックカウントに関して、私のアルゴリズムは平均で406クロックサイクルを必要とし、これには統計の収集と実際のソートルーチンへの2つの呼び出しの実行が含まれます-各ギャップに1つです。これは、Athlon M300モバイルのコンパイラOpenWatcomにあります。
Olof Forshell、2012年

1
「規則的なランダム分布」とは、ソートされた実際のデータのすべての組み合わせが、確率が等しくない場合があることを意味します。すべての組み合わせの確率が等しくない場合、特定の分布が発生する可能性が高い回数を平均で考慮する必要があるため、統計は壊れます。クロックカウントについては、この種類の他の実装(上記のリンク)を試してテストシステムで実行すると、比較の基準があり、選択したもののパフォーマンスを確認できます。
クリス

1

私はこれが古い質問であることを知っています。

しかし、私は共有したい別の種類のソリューションを書いただけです。
入れ子になったMIN MAXだけを使用して、

それぞれ114を使用しているため、高速ではなく、
単純に次のように75に減らすことができます-> pastebin

しかし、それはもはや純粋な最大値ではありません。

うまくいくかもしれないのは、AVXで一度に複数の整数に対して最小/最大を実行することです

PMINSWリファレンス

#include <stdio.h>

static __inline__ int MIN(int a, int b){
int result =a;
__asm__ ("pminsw %1, %0" : "+x" (result) : "x" (b));
return result;
}
static __inline__ int MAX(int a, int b){
int result = a;
__asm__ ("pmaxsw %1, %0" : "+x" (result) : "x" (b));
return result;
}
static __inline__ unsigned long long rdtsc(void){
  unsigned long long int x;
__asm__ volatile (".byte 0x0f, 0x31" :
  "=A" (x));
  return x;
}

#define MIN3(a, b, c) (MIN(MIN(a,b),c))
#define MIN4(a, b, c, d) (MIN(MIN(a,b),MIN(c,d)))

static __inline__ void sort6(int * in) {
  const int A=in[0], B=in[1], C=in[2], D=in[3], E=in[4], F=in[5];

  in[0] = MIN( MIN4(A,B,C,D),MIN(E,F) );

  const int
  AB = MAX(A, B),
  AC = MAX(A, C),
  AD = MAX(A, D),
  AE = MAX(A, E),
  AF = MAX(A, F),
  BC = MAX(B, C),
  BD = MAX(B, D),
  BE = MAX(B, E),
  BF = MAX(B, F),
  CD = MAX(C, D),
  CE = MAX(C, E),
  CF = MAX(C, F),
  DE = MAX(D, E),
  DF = MAX(D, F),
  EF = MAX(E, F);

  in[1] = MIN4 (
  MIN4( AB, AC, AD, AE ),
  MIN4( AF, BC, BD, BE ),
  MIN4( BF, CD, CE, CF ),
  MIN3( DE, DF, EF)
  );

  const int
  ABC = MAX(AB,C),
  ABD = MAX(AB,D),
  ABE = MAX(AB,E),
  ABF = MAX(AB,F),
  ACD = MAX(AC,D),
  ACE = MAX(AC,E),
  ACF = MAX(AC,F),
  ADE = MAX(AD,E),
  ADF = MAX(AD,F),
  AEF = MAX(AE,F),
  BCD = MAX(BC,D),
  BCE = MAX(BC,E),
  BCF = MAX(BC,F),
  BDE = MAX(BD,E),
  BDF = MAX(BD,F),
  BEF = MAX(BE,F),
  CDE = MAX(CD,E),
  CDF = MAX(CD,F),
  CEF = MAX(CE,F),
  DEF = MAX(DE,F);

  in[2] = MIN( MIN4 (
  MIN4( ABC, ABD, ABE, ABF ),
  MIN4( ACD, ACE, ACF, ADE ),
  MIN4( ADF, AEF, BCD, BCE ),
  MIN4( BCF, BDE, BDF, BEF )),
  MIN4( CDE, CDF, CEF, DEF )
  );


  const int
  ABCD = MAX(ABC,D),
  ABCE = MAX(ABC,E),
  ABCF = MAX(ABC,F),
  ABDE = MAX(ABD,E),
  ABDF = MAX(ABD,F),
  ABEF = MAX(ABE,F),
  ACDE = MAX(ACD,E),
  ACDF = MAX(ACD,F),
  ACEF = MAX(ACE,F),
  ADEF = MAX(ADE,F),
  BCDE = MAX(BCD,E),
  BCDF = MAX(BCD,F),
  BCEF = MAX(BCE,F),
  BDEF = MAX(BDE,F),
  CDEF = MAX(CDE,F);

  in[3] = MIN4 (
  MIN4( ABCD, ABCE, ABCF, ABDE ),
  MIN4( ABDF, ABEF, ACDE, ACDF ),
  MIN4( ACEF, ADEF, BCDE, BCDF ),
  MIN3( BCEF, BDEF, CDEF )
  );

  const int
  ABCDE= MAX(ABCD,E),
  ABCDF= MAX(ABCD,F),
  ABCEF= MAX(ABCE,F),
  ABDEF= MAX(ABDE,F),
  ACDEF= MAX(ACDE,F),
  BCDEF= MAX(BCDE,F);

  in[4]= MIN (
  MIN4( ABCDE, ABCDF, ABCEF, ABDEF ),
  MIN ( ACDEF, BCDEF )
  );

  in[5] = MAX(ABCDE,F);
}

int main(int argc, char ** argv) {
  int d[6][6] = {
    {1, 2, 3, 4, 5, 6},
    {6, 5, 4, 3, 2, 1},
    {100, 2, 300, 4, 500, 6},
    {100, 2, 3, 4, 500, 6},
    {1, 200, 3, 4, 5, 600},
    {1, 1, 2, 1, 2, 1}
  };

  unsigned long long cycles = rdtsc();
  for (int i = 0; i < 6; i++) {
    sort6(d[i]);
  }
  cycles = rdtsc() - cycles;
  printf("Time is %d\n", (unsigned)cycles);

  for (int i = 0; i < 6; i++) {
    printf("d%d : %d %d %d %d %d %d\n", i,
     d[i][0], d[i][1], d[i][2],
     d[i][3], d[i][4], d[i][5]);
  }
}

編集:
Rex Kerr'sに触発されたランクオーダーソリューション、上記の混乱よりもはるかに高速

static void sort6(int *o) {
const int 
A=o[0],B=o[1],C=o[2],D=o[3],E=o[4],F=o[5];
const unsigned char
AB = A>B, AC = A>C, AD = A>D, AE = A>E,
          BC = B>C, BD = B>D, BE = B>E,
                    CD = C>D, CE = C>E,
                              DE = D>E,
a =          AB + AC + AD + AE + (A>F),
b = 1 - AB      + BC + BD + BE + (B>F),
c = 2 - AC - BC      + CD + CE + (C>F),
d = 3 - AD - BD - CD      + DE + (D>F),
e = 4 - AE - BE - CE - DE      + (E>F);
o[a]=A; o[b]=B; o[c]=C; o[d]=D; o[e]=E;
o[15-a-b-c-d-e]=F;
}

1
新しい解決策を見るのはいつもうれしい。簡単な最適化が可能なようです。結局、それはソーティングネットワークとそれほど異なることを証明しないかもしれません。
クリス

はい、MINとMAXの数はおそらく減らすことができます。たとえば、MIN(AB、CD)は数回繰り返されますが、それらをたくさん減らすのは難しいと思います。テストケースを追加しました。
PrincePolka 2017

pmin / maxswは、パックされた16ビット符号付き整数(int16_t)で動作します。しかし、C関数はそれが配列をソートすると主張していますint(これは、そのasm構文をサポートするすべてのC実装で32ビットです)。上半分に0しかない小さな正の整数のみでテストしましたか?これでintうまくいきます... SSE4.1 pmin/maxsd(d = dword)が必要です。 felixcloutier.com/x86/pminsd:pminsqまたはpminusdfor uint32_t
Peter Cordes

1

少なくとも私のシステムでは、以下に定義されている関数sort6_iterator()sort6_iterator_local()定義されている関数の両方が、上記の現在のレコードホルダーよりも少なくとも同じくらい速く、頻繁に著しく高速であることがわかりました。

#define MIN(x, y) (x<y?x:y)
#define MAX(x, y) (x<y?y:x)

template<class IterType> 
inline void sort6_iterator(IterType it) 
{
#define SWAP(x,y) { const auto a = MIN(*(it + x), *(it + y)); \
  const auto b = MAX(*(it + x), *(it + y)); \
  *(it + x) = a; *(it + y) = b; }

  SWAP(1, 2) SWAP(4, 5)
  SWAP(0, 2) SWAP(3, 5)
  SWAP(0, 1) SWAP(3, 4)
  SWAP(1, 4) SWAP(0, 3)
  SWAP(2, 5) SWAP(1, 3)
  SWAP(2, 4)
  SWAP(2, 3)
#undef SWAP
}

私はこの関数を渡しました std::vectorにタイミングコードでのイテレータ。

このようなコメントなどから)イテレータを使用すると、イテレータが参照するメモリで発生する可能性と発生しない可能性について、g ++が特定の保証を提供するのではないかと疑っています。並べ替えコードをより最適化します(たとえば、ポインターを使用すると、コンパイラーはすべてのポインターが異なるメモリ位置を指しているとは確信できません)。私の記憶が正しければ、これも次のような多くのSTLアルゴリズムの理由の一部ですstd::sort()一般に卑劣なパフォーマンスを発揮。

また、sort6_iterator()されたいくつかの(関数が呼び出されるコンテキストに応じて再度、)時間が一貫してそれらをソートする前に、次のソート機能、ローカル変数にコピーデータが上回りました。1定義されているローカル変数は6つしかないため、これらのローカル変数がプリミティブである場合、それらは実際にはRAMに格納されることはなく、関数呼び出しの最後までCPUのレジスターにのみ格納されることに注意してください。機能が速い。(また、コンパイラーが、個別のローカル変数がメモリー内の個別の場所にあることを認識するのにも役立ちます)。

template<class IterType> 
inline void sort6_iterator_local(IterType it) 
{
#define SWAP(x,y) { const auto a = MIN(data##x, data##y); \
  const auto b = MAX(data##x, data##y); \
  data##x = a; data##y = b; }
//DD = Define Data
#define DD1(a)   auto data##a = *(it + a);
#define DD2(a,b) auto data##a = *(it + a), data##b = *(it + b);
//CB = Copy Back
#define CB(a) *(it + a) = data##a;

  DD2(1,2)    SWAP(1, 2)
  DD2(4,5)    SWAP(4, 5)
  DD1(0)      SWAP(0, 2)
  DD1(3)      SWAP(3, 5)
  SWAP(0, 1)  SWAP(3, 4)
  SWAP(1, 4)  SWAP(0, 3)   CB(0)
  SWAP(2, 5)  CB(5)
  SWAP(1, 3)  CB(1)
  SWAP(2, 4)  CB(4)
  SWAP(2, 3)  CB(2)        CB(3)
#undef CB
#undef DD2
#undef DD1
#undef SWAP
}

注定義があることSWAP()として、次のいくつかのわずかに良いパフォーマンスが倍の結果を、それがわずかに悪化し、性能やパフォーマンスを無視できる程度に差が生じ、ほとんどの時間けれども。

#define SWAP(x,y) { const auto a = MIN(data##x, data##y); \
  data##y = MAX(data##x, data##y); \
  data##x = a; }

プリミティブデータタイプの並べ替えアルゴリズムが必要な場合、gcc -O3は、並べ替え関数の呼び出しが1に現れるコンテキストに関係なく、一貫して最適化に優れています。入力を渡す方法に応じて、次の2つのいずれかを試してください。アルゴリズム:

template<class T> inline void sort6(T it) {
#define SORT2(x,y) {if(data##x>data##y){auto a=std::move(data##y);data##y=std::move(data##x);data##x=std::move(a);}}
#define DD1(a)   register auto data##a=*(it+a);
#define DD2(a,b) register auto data##a=*(it+a);register auto data##b=*(it+b);
#define CB1(a)   *(it+a)=data##a;
#define CB2(a,b) *(it+a)=data##a;*(it+b)=data##b;
  DD2(1,2) SORT2(1,2)
  DD2(4,5) SORT2(4,5)
  DD1(0)   SORT2(0,2)
  DD1(3)   SORT2(3,5)
  SORT2(0,1) SORT2(3,4) SORT2(2,5) CB1(5)
  SORT2(1,4) SORT2(0,3) CB1(0)
  SORT2(2,4) CB1(4)
  SORT2(1,3) CB1(1)
  SORT2(2,3) CB2(2,3)
#undef CB1
#undef CB2
#undef DD1
#undef DD2
#undef SORT2
}

または、変数を参照で渡したい場合は、これを使用します(以下の関数は、最初の5行が上記のものと異なります)。

template<class T> inline void sort6(T& e0, T& e1, T& e2, T& e3, T& e4, T& e5) {
#define SORT2(x,y) {if(data##x>data##y)std::swap(data##x,data##y);}
#define DD1(a)   register auto data##a=e##a;
#define DD2(a,b) register auto data##a=e##a;register auto data##b=e##b;
#define CB1(a)   e##a=data##a;
#define CB2(a,b) e##a=data##a;e##b=data##b;
  DD2(1,2) SORT2(1,2)
  DD2(4,5) SORT2(4,5)
  DD1(0)   SORT2(0,2)
  DD1(3)   SORT2(3,5)
  SORT2(0,1) SORT2(3,4) SORT2(2,5) CB1(5)
  SORT2(1,4) SORT2(0,3) CB1(0)
  SORT2(2,4) CB1(4)
  SORT2(1,3) CB1(1)
  SORT2(2,3) CB2(2,3)
#undef CB1
#undef CB2
#undef DD1
#undef DD2
#undef SORT2
}

registerキーワードを使用する理由は、これがレジスタにこれらの値が必要であることを知っている数少ないことの1つだからです。なしregisterでは、コンパイラーはほとんどの場合これを理解しますが、そうでない場合もあります。registerキーワードを使用すると、この問題の解決に役立ちます。ただし、registerコードを高速化するよりも低速にする可能性が高いため、通常はキーワードを使用しないでください。

また、テンプレートの使用にも注意してください。inlineキーワードを使用しても、テンプレート関数は通常、バニラC関数よりもgccによって積極的に最適化されるため、これは意図的に行われます(これは、gccがバニラC関数の関数ポインターを処理する必要があるが、テンプレート関数を処理する必要がないためです)。

  1. さまざまな並べ替え関数のタイミングを計っているときに、並べ替え関数の呼び出しが行われたコンテキスト(つまり、周囲のコード)がパフォーマンスに大きな影響を与えていることに気付きました。たとえば、プログラムが十分に単純な場合、通常、並べ替え関数にポインターを渡す場合と、イテレーターを渡す場合とで、パフォーマンスに大きな違いはありませんでした。それ以外の場合、イテレータを使用すると、通常は著しくパフォーマンスが向上し、(少なくともこれまでの私の経験では)著しくパフォーマンスが低下することはありませんでした。これは、g ++が十分に単純なコードをグローバルに最適化できるためと考えられます。

0

「ソート済みリストのマージ」ソートを試してください。:) 2つの配列を使用します。小規模および大規模アレイに最速。
連結する場合は、挿入位置のみを確認します。比較する必要のない他の大きな値(cmp = ab> 0)。
4つの数値の場合、システムは4-5 cmp(〜4.6)または3-6 cmp(〜4.9)を使用できます。バブルソートは6 cmp(6)を使用します。大きな数の遅いコードのcmpがたくさん。
このコードは5 cmpを使用します(MSLソートではありません)。
if (cmp(arr[n][i+0],arr[n][i+1])>0) {swap(n,i+0,i+1);} if (cmp(arr[n][i+2],arr[n][i+3])>0) {swap(n,i+2,i+3);} if (cmp(arr[n][i+0],arr[n][i+2])>0) {swap(n,i+0,i+2);} if (cmp(arr[n][i+1],arr[n][i+3])>0) {swap(n,i+1,i+3);} if (cmp(arr[n][i+1],arr[n][i+2])>0) {swap(n,i+1,i+2);}

主要なMSL 9 8 7 6 5 4 3 2 1 0 89 67 45 23 01 ... concat two sorted lists, list length = 1 6789 2345 01 ... concat two sorted lists, list length = 2 23456789 01 ... concat two sorted lists, list length = 4 0123456789 ... concat two sorted lists, list length = 8

jsコード

function sortListMerge_2a(cmp)	
{
var step, stepmax, tmp, a,b,c, i,j,k, m,n, cycles;
var start = 0;
var end   = arr_count;
//var str = '';
cycles = 0;
if (end>3)
	{
	stepmax = ((end - start + 1) >> 1) << 1;
	m = 1;
	n = 2;
	for (step=1;step<stepmax;step<<=1)	//bounds 1-1, 2-2, 4-4, 8-8...
		{
		a = start;
		while (a<end)
			{
			b = a + step;
			c = a + step + step;
			b = b<end ? b : end;
			c = c<end ? c : end;
			i = a;
			j = b;
			k = i;
			while (i<b && j<c)
				{
				if (cmp(arr[m][i],arr[m][j])>0)
					{arr[n][k] = arr[m][j]; j++; k++;}
				else	{arr[n][k] = arr[m][i]; i++; k++;}
				}
			while (i<b)
				{arr[n][k] = arr[m][i]; i++; k++;
}
			while (j<c)
				{arr[n][k] = arr[m][j]; j++; k++;
}
			a = c;
			}
		tmp = m; m = n; n = tmp;
		}
	return m;
	}
else
	{
	// sort 3 items
	sort10(cmp);
	return m;
	}
}


0

使用法cmp == 0で4項目をソートします。cmpの数は〜4.34(FFネイティブは〜4.52)ですが、リストのマージより3倍の時間がかかります。ただし、数値が大きい場合やテキストが大きい場合は、cmp操作の数を減らした方がよいでしょう。編集:バグを修正

オンラインテストhttp://mlich.zam.slu.cz/js-sort/x-sort-x2.htm

function sort4DG(cmp,start,end,n) // sort 4
{
var n     = typeof(n)    !=='undefined' ? n   : 1;
var cmp   = typeof(cmp)  !=='undefined' ? cmp   : sortCompare2;
var start = typeof(start)!=='undefined' ? start : 0;
var end   = typeof(end)  !=='undefined' ? end   : arr[n].length;
var count = end - start;
var pos = -1;
var i = start;
var cc = [];
// stabilni?
cc[01] = cmp(arr[n][i+0],arr[n][i+1]);
cc[23] = cmp(arr[n][i+2],arr[n][i+3]);
if (cc[01]>0) {swap(n,i+0,i+1);}
if (cc[23]>0) {swap(n,i+2,i+3);}
cc[12] = cmp(arr[n][i+1],arr[n][i+2]);
if (!(cc[12]>0)) {return n;}
cc[02] = cc[01]==0 ? cc[12] : cmp(arr[n][i+0],arr[n][i+2]);
if (cc[02]>0)
    {
    swap(n,i+1,i+2); swap(n,i+0,i+1); // bubble last to top
    cc[13] = cc[23]==0 ? cc[12] : cmp(arr[n][i+1],arr[n][i+3]);
    if (cc[13]>0)
        {
        swap(n,i+2,i+3); swap(n,i+1,i+2); // bubble
        return n;
        }
    else    {
    cc[23] = cc[23]==0 ? cc[12] : (cc[01]==0 ? cc[30] : cmp(arr[n][i+2],arr[n][i+3]));  // new cc23 | c03 //repaired
        if (cc[23]>0)
            {
            swap(n,i+2,i+3);
            return n;
            }
        return n;
        }
    }
else    {
    if (cc[12]>0)
        {
        swap(n,i+1,i+2);
        cc[23] = cc[23]==0 ? cc[12] : cmp(arr[n][i+2],arr[n][i+3]); // new cc23
        if (cc[23]>0)
            {
            swap(n,i+2,i+3);
            return n;
            }
        return n;
        }
    else    {
        return n;
        }
    }
return n;
}

1
ユースケースは、質問の最初のコンテキストとは少し異なります。固定長のソートでは、詳細が重要であり、スワップのcmpをカウントするだけでは不十分です。時間を消費する実際の種類ではなくても、initでtypeof()を呼び出すまったく別の簡単なものであったとしても、私は驚かれることもありません。Javascriptを使用して実際の時刻を測定する方法がわかりません。たぶんノード?
クリス、

0

多分私パーティーに遅れますが、少なくとも私の貢献は新しいアプローチです。

  • コードは本当にインライン化されるべきです
  • インライン化しても分岐が多すぎる
  • 分析部分は基本的にO(N(N-1))で、N = 6の場合は問題ないようです。
  • コストがswap(のコストに対してcompare)高くなる場合、コードはより効果的です。
  • 静的関数がインライン化されることを信頼しています。
  • 方法はランクソートに関連しています
    • ランクの代わりに、相対ランク(オフセット)が使用されます。
    • ランクの合計は、順列グループのすべてのサイクルでゼロです。
    • SWAP()2つの要素を使用する代わりに、1つのtempと1つの(register-> register)スワップ(新しい<-old)のみを必要とするサイクルが追跡されます。

更新:コードを少し変更しました。C++コンパイラを使用してCコードをコンパイルする人もいます...

#include <stdio.h>

#if WANT_CHAR
typedef signed char Dif;
#else
typedef signed int Dif;
#endif

static int walksort (int *arr, int cnt);
static void countdifs (int *arr, Dif *dif, int cnt);
static void calcranks(int *arr, Dif *dif);

int wsort6(int *arr);

void do_print_a(char *msg, int *arr, unsigned cnt)
{
fprintf(stderr,"%s:", msg);
for (; cnt--; arr++) {
        fprintf(stderr, " %3d", *arr);
        }
fprintf(stderr,"\n");
}

void do_print_d(char *msg, Dif *arr, unsigned cnt)
{
fprintf(stderr,"%s:", msg);
for (; cnt--; arr++) {
        fprintf(stderr, " %3d", (int) *arr);
        }
fprintf(stderr,"\n");
}

static void inline countdifs (int *arr, Dif *dif, int cnt)
{
int top, bot;

for (top = 0; top < cnt; top++ ) {
        for (bot = 0; bot < top; bot++ ) {
                if (arr[top] < arr[bot]) { dif[top]--; dif[bot]++; }
                }
        }
return ;
}
        /* Copied from RexKerr ... */
static void inline calcranks(int *arr, Dif *dif){

dif[0] =     (arr[0]>arr[1])+(arr[0]>arr[2])+(arr[0]>arr[3])+(arr[0]>arr[4])+(arr[0]>arr[5]);
dif[1] = -1+ (arr[1]>=arr[0])+(arr[1]>arr[2])+(arr[1]>arr[3])+(arr[1]>arr[4])+(arr[1]>arr[5]);
dif[2] = -2+ (arr[2]>=arr[0])+(arr[2]>=arr[1])+(arr[2]>arr[3])+(arr[2]>arr[4])+(arr[2]>arr[5]);
dif[3] = -3+ (arr[3]>=arr[0])+(arr[3]>=arr[1])+(arr[3]>=arr[2])+(arr[3]>arr[4])+(arr[3]>arr[5]);
dif[4] = -4+ (arr[4]>=arr[0])+(arr[4]>=arr[1])+(arr[4]>=arr[2])+(arr[4]>=arr[3])+(arr[4]>arr[5]);
dif[5] = -(dif[0]+dif[1]+dif[2]+dif[3]+dif[4]);
}

static int walksort (int *arr, int cnt)
{
int idx, src,dst, nswap;

Dif difs[cnt];

#if WANT_REXK
calcranks(arr, difs);
#else
for (idx=0; idx < cnt; idx++) difs[idx] =0;
countdifs(arr, difs, cnt);
#endif
calcranks(arr, difs);

#define DUMP_IT 0
#if DUMP_IT
do_print_d("ISteps ", difs, cnt);
#endif

nswap = 0;
for (idx=0; idx < cnt; idx++) {
        int newval;
        int step,cyc;
        if ( !difs[idx] ) continue;
        newval = arr[idx];
        cyc = 0;
        src = idx;
        do      {
                int oldval;
                step = difs[src];
                difs[src] =0;
                dst = src + step;
                cyc += step ;
                if(dst == idx+1)idx=dst;
                oldval = arr[dst];
#if (DUMP_IT&1)
                fprintf(stderr, "[Nswap=%d] Cyc=%d Step=%2d Idx=%d  Old=%2d New=%2d #### Src=%d Dst=%d[%2d]->%2d <-- %d\n##\n"
                        , nswap, cyc, step, idx, oldval, newval
                        , src, dst, difs[dst], arr[dst]
                        , newval  );
                do_print_a("Array ", arr, cnt);
                do_print_d("Steps ", difs, cnt);
#endif

                arr[dst] = newval;
                newval = oldval;
                nswap++;
                src = dst;
                } while( cyc);
        }

return nswap;
}
/*************/
int wsort6(int *arr)
{
return walksort(arr, 6);
}

バブルソートのように見えます。最も遅い実装の潜在的な良い候補ですが、コードで作業することでそれほど大きな違いが生じるかどうかを知ることは、まだ興味深いことです。他のコードと同じ形式でコードを記述してください。これにより、ベンチマークを実行できます。
クリス

@kriss en.wikipedia.org/wiki/Permutation_groupこれは確かにバブルソートではありませ。コードは指定された順列のサイクルを検出し、これらのサイクルをウォークして、各要素を最終的な場所に配置します。最後のwsort6()関数は正しいインターフェースを持っています。
joop 2018

@joop:私の悪い、確かにバブルソートはありません。その文脈で言われていることは、コードが他の現在の実装よりもはるかに悪いことを私はまだ期待していることです。ところで、ランクオーダーソリューションは、すべてのアイテムの最終的な位置を直接見つけるため、スワップの数に関して最適です。ここでのように、ソートされたすべての数値が異なるという仮説を削除したときに、walksortが機能するかどうかも不明です。コードをベンチマークするには、コードをトレースする必要があります。また、私は通常C ++コンパイラーでコンパイルしているので、OPが変数「new」を呼び出したため、コードは機能しません(これにより構文の強調表示が解除されます)。
クリス

この方法はランクの順序に非常に近く、最終的な割り当てのみが適切に行わます。ranksを除いo1..o5て、2番目のtemp e[6]配列は必要ありません。そして:C ++コンパイラでCコードをコンパイルし、コードを非難しますか?
joop

@greybeard:ありがとう、前にスペースを追加しました#include。修正済み
wildplasser 2018

0
//Bruteforce compute unrolled count dumbsort(min to 0-index)
void bcudc_sort6(int* a)
{
    int t[6] = {0};
    int r1,r2;

    r1=0;
    r1 += (a[0] > a[1]);
    r1 += (a[0] > a[2]);
    r1 += (a[0] > a[3]);
    r1 += (a[0] > a[4]);
    r1 += (a[0] > a[5]);
    while(t[r1]){r1++;}
    t[r1] = a[0];

    r2=0;
    r2 += (a[1] > a[0]);
    r2 += (a[1] > a[2]);
    r2 += (a[1] > a[3]);
    r2 += (a[1] > a[4]);
    r2 += (a[1] > a[5]);
    while(t[r2]){r2++;} 
    t[r2] = a[1];

    r1=0;
    r1 += (a[2] > a[0]);
    r1 += (a[2] > a[1]);
    r1 += (a[2] > a[3]);
    r1 += (a[2] > a[4]);
    r1 += (a[2] > a[5]);
    while(t[r1]){r1++;}
    t[r1] = a[2];

    r2=0;
    r2 += (a[3] > a[0]);
    r2 += (a[3] > a[1]);
    r2 += (a[3] > a[2]);
    r2 += (a[3] > a[4]);
    r2 += (a[3] > a[5]);
    while(t[r2]){r2++;} 
    t[r2] = a[3];

    r1=0;
    r1 += (a[4] > a[0]);
    r1 += (a[4] > a[1]);
    r1 += (a[4] > a[2]);
    r1 += (a[4] > a[3]);
    r1 += (a[4] > a[5]);
    while(t[r1]){r1++;}
    t[r1] = a[4];

    r2=0;
    r2 += (a[5] > a[0]);
    r2 += (a[5] > a[1]);
    r2 += (a[5] > a[2]);
    r2 += (a[5] > a[3]);
    r2 += (a[5] > a[4]);
    while(t[r2]){r2++;} 
    t[r2] = a[5];

    a[0]=t[0];
    a[1]=t[1];
    a[2]=t[2];
    a[3]=t[3];
    a[4]=t[4];
    a[5]=t[5];
}

static __inline__ void sort6(int* a)
{
    #define wire(x,y); t = a[x] ^ a[y] ^ ( (a[x] ^ a[y]) & -(a[x] < a[y]) ); a[x] = a[x] ^ t; a[y] = a[y] ^ t;
    register int t;

    wire( 0, 1); wire( 2, 3); wire( 4, 5);
    wire( 3, 5); wire( 0, 2); wire( 1, 4);
    wire( 4, 5); wire( 2, 3); wire( 0, 1); 
    wire( 3, 4); wire( 1, 2); 
    wire( 2, 3);

    #undef wire
}

速度に関係なく、動作しますか?ブルートフォースソートでは、ループが怪しいです。ソートされた値にゼロがある場合、それらは機能しないように思えます。
クリス

1
t [6]配列は0x0に初期化されます。したがって、0x0の値のキーがどこに書き込まれるかは問題ではありません。
フラン

-1

まあ、それが6要素のみであり、並列処理を活用できる場合、条件付き分岐などを最小限にしたい場合など。すべての組み合わせを生成して順序をテストしないのはなぜですか。一部のアーキテクチャではかなり高速になる可能性があることを思いつきます(メモリが事前に割り当てられている限り)


9
720の注文があり、高速バージョンは100サイクル未満です。大規模な並列処理を活用できる場合でも、このような小さな時間スケールでは、スレッドの作成と同期のコストは、1つのコアで配列を並べ替えるコストを超える可能性があります。
ケビン株

-3

以下は、3つの異なる分類アルゴリズムのクラスを表す3つの典型的な分類方法です。

Insertion Sort: Θ(n^2)

Heap Sort: Θ(n log n)

Count Sort: Θ(3n)

しかし、最速のソートアルゴリズムに関するStefan Nelssonの議論をチェックしてくださいここで、彼はO(n log log n).. に至るソリューションについて議論し、Cでの実装を確認します。

このセミリニアソートアルゴリズムは、1995年に論文で発表されました。

A.アンダーソン、T。ハゲラップ、S。ニルソン、およびR.ラマン。線形時間でソートしますか?第27回コンピューティング理論に関する年次ACMシンポジウムの議事録、427-436ページ、1995年。


8
これは興味深いですが、要点は別です。Big-Θは、一定の要因を隠し、問題のサイズ(n)が大きくなるときの傾向を示すことを目的としています。ここでの問題は完全に固定問題サイズ(n = 6)についてであり、一定の要因を考慮に入れています。
クリス2013

@kriss正解です。私の比較は漸近的であるため、実際の比較では、その場合の方が速いかそうでないかが示されます
Khaled.K

4
アルゴリズムごとに異なるK乗法定数(およびC加法定数)が隠されているため、結論を出すことはできません。つまり、k0、c0は挿入ソート、k1、c1はヒープソートなどです。これらの定数はすべて実際には異なります(物理的な用語で言えば、各アルゴリズムには独自の「摩擦係数」があると言えます)。この場合(または任意の固定nの場合)、アルゴリズムが実際に高速であると結論付けることはできません。
クリス、2013
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.