web-dev-qa-db-ja.com

最速の固定長6 int配列

別のStack Overflowの質問に答える( this one )興味深い副次的な問題につまずいた。 6個の整数の配列を並べ替える最速の方法は何ですか?

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

  • ライブラリが利用可能であると仮定することはできません(呼び出し自体にコストがかかります)。
  • 命令パイプライン(veryのコストが高い)を空にしないために、おそらく分岐、ジャンプ、その他のあらゆる種類の制御フローの中断(&&のシーケンスポイントの後ろに隠れているようなもの)または||)。
  • 部屋に制約があり、レジスタとメモリの使用を最小限に抑えることが問題です。理想的には、おそらくソートが最適です。

本当にこの質問は、ソースの長さではなく実行時間を最小限にすることを目標とするゴルフの一種です。私はそれを本のタイトルで使用されている「Zening」コードと呼んでいます Zen of Code最適化 by Michael Abrash およびその sequels

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

  • この例はシンプルで理解と測定が簡単で、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);
}

生の結果

バリアントの数が増えてきているので、それらすべてを here にあるテストスイートに集めました。実際に使用されたテストは、ケビン・ストックのおかげで、上に示したテストよりも少し単純です。独自の環境でコンパイルして実行できます。異なるターゲットアーキテクチャ/コンパイラでの動作に非常に興味があります。 (OKみんな、答えに入れて、新しい結果セットのすべての貢献者に+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
  • ソーティングネットワーク(ダニエルシュツッツバッハ):111.68
  • ネットワークの並べ替え(ポールR):66.36
  • Fast Swapを使用したネットワーク12のソート:58.86
  • 並べ替えネットワーク12の並べ替えスワップ:53.74
  • 並べ替えネットワーク12が並べ替えられましたSimple Swap:31.54
  • 高速スワップ付き並べ替えネットワークの並べ替え:31.54
  • 高速スワップV2を使用した並べ替えネットワークの並べ替え:33.63
  • インラインバブルソート(パオロボンジーニ):48.85
  • 展開された挿入ソート(パオロボンジーニ):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
  • ソーティングネットワーク(ダニエルシュツッツバッハ):115.57
  • ネットワークの並べ替え(ポールR):64.44
  • Fast Swapを使用したネットワーク12のソート:61.98
  • 並べ替えネットワーク12が並べ替えられたスワップ:54.67
  • 並べ替えネットワーク12が並べ替えられましたSimple Swap:31.54
  • 高速スワップ付き並べ替えネットワークの並べ替え:31.24
  • 高速スワップV2を使用した並べ替えネットワークの並べ替え:33.07
  • インラインバブルソート(パオロボンジーニ):45.79
  • 展開挿入ソート(パオロボンジーニ):80.15

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

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

挿入ソート(Daniel Stutzbach)

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

仕分けネットワーク(Daniel Stutzbach)

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

ネットワークのソート(ポールR)

これまでのところ最高。テストに使用した実際のコードは here です。他のソーティングネットワークの実装の2倍近くの速度である理由はまだわかりません。パラメータの受け渡し?高速マックス?

高速スワップを使用したネットワーク12 SWAPの並べ替え

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

ブランチレススワップは、PPCアーキテクチャでifを使用する単純なスワップよりもはるかに(4倍)効率が低いように見えることにも注目してください。

ライブラリqsortの呼び出し

別の参照ポイントを与えるために、提案されたように、ライブラリqsortを呼び出すことも試みました(コードは here です)。予想通り、はるかに遅いです: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.3を使用し、最後のコードはこの特定のコードの最適化がはるかに優れているようです(他の提案ではほとんど違いはありませんでした)。

update

上記の公開された図が示すように、この効果は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%高速に動作しています。

Simple Swapを使用したネットワーク12のソート

元の投稿Steinar H. Gundersonが提案した1年後、コンパイラを裏切って、スワップコードをシンプルに保つことを試みるべきではない、と。結果として得られるコードは約40%高速であるため、実際に良い考えです。彼はまた、x86インラインアセンブリコードを使用して手動で最適化されたスワップを提案しました。最も驚くべきこと(プログラマーの心理学に関するボリュームと言われています)は、1年前に誰もそのバージョンのスワップを試していなかったことです。テストに使用したコードは here です。他の人は、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サイクルになります。私はそれを驚くほど速く呼び出します。その他の改善は可能ですか?

387
kriss

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

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

投稿したアルゴリズムは挿入ソートに似ていますが、より多くの比較を犠牲にしてスワップの数を最小限にしたようです。ただし、分岐は命令パイプラインを停止させる可能性があるため、比較はスワップよりもはるかに高価です。

挿入ソートの実装は次のとおりです。

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
}
158

ネットワークの並べ替え を使用した実装を次に示します。

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);  
}

これには、非常に効率的なブランチレスminおよびmax実装が本当に必要です。これは、このコードが結果的に要約するものであるためです-minおよびmax操作のシーケンス(それぞれ合計13)。これは読者の演習として残しておきます。

この実装は、ベクトル化(SIMD-ほとんどのSIMD ISAにベクトルの最小/最大命令がある)およびGPU実装(CUDA-分岐のないワープ発散などの問題はありません)に適していることに注意してください。

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

60
Paul R

これらは整数であり、比較は高速であるため、それぞれのランク順を直接計算しないのはなぜですか。

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];
}
44
Rex Kerr

一年遅れてパーティーに行ったように見えるが、ここに行く...

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

より良い順序があるかもしれませんが、最高のパフォーマンスとして与えられた再順序付けされたスワップネットワークと同じスワップ順序を使用しました。さらに時間があれば、一連の順列を生成してテストします。

テストコードを変更して4000を超えるアレイを検討し、各アレイをソートするのに必要な平均サイクル数を示しました。 i5-650では、元の並べ替えられた並べ替えネットワークが-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
35
Kevin Stock

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

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

このアプローチには、trulyブランチレス関数を生成するという副作用もありました。ジャンプ命令は一切ありません。

この実装は、現在最も高速なオプションとしてマークされている実装(「Sorting Networks 12 with Simple Swap」よりも約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);
15
Joe Crivello

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

そうは言っても、バイトニックネットワークソリューションの改善も非常に簡単です。単純にmin/max/SWAPを変更します

#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%高速になります(Debian gcc 4.4.5 with -O2、AMD64、Core i7)。

14

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

#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がどのように機能するかに注目し、共通の部分式を明示的に引き出します。これにより、最小マクロと最大マクロが完全になくなります。

13
phkahler

ベンチマークと実際のコンパイラ生成アセンブリを確認せずに、最小/最大を最適化しないでください。 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]など)を完全に操作した場合よりもさらに優れています。これを書いているのは、強力な最適化を想定しているにもかかわらず、コンパイラをmin/maxで裏切るようにしているからです。 :)

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

完全を期すために、展開されたバブルソートと挿入ソートも可能です。バブルの並べ替えは次のとおりです。

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の挿入ソートよりも高速であり、ITERは3つの命令(SWAPの場合は4)のみで実行できるため、GPUまたは予測のあるコンピューターで特に適しています。たとえば、ARMアセンブリのt = d[2]; ITER(1); ITER(0);行は次のとおりです。

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

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

12
Paolo Bonzini

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

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

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

挿入ソート(Daniel Stutzbach):108

挿入ソートを展開:51

仕分けネットワーク(ダニエルシュツッツバッハ):26

並べ替えネットワーク(ポールR):85

高速スワップを使用したネットワーク12の並べ替え:117

Sorting Networks 12が並べ替えられたスワップ:116

ランク順:56

11
jheriko

XORスワップは、スワッピング関数で役立ちます。

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

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

7
naj

これを試して、これらの例から学ぶことを楽しみにしていますが、最初に1.5 GHz PPC Powerbook G4 w/1 GB DDR RAMからいくつかのタイミングを確認します。 (タイミングについては、 http://www.mcs.anl.gov/~kazutomo/rdtsc.html からPPCに似たrdtscのようなタイマーを借りました。)プログラムは数回あり、絶対的な結果はさまざまでしたが、一貫して最速のテストは「挿入ソート(ダニエルシュツッツバッハ)」で、「挿入ソートを展開」は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
5
Nico

このスレッドへの私の貢献は次のとおりです。一意の値を含む6メンバーのintベクトル(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ビット環境がありません。

4
Olof Forshell

私は超後期だと知っていますが、いくつかの異なるソリューションを試すことに興味がありました。まず、そのペーストをクリーンアップし、コンパイルして、リポジトリに配置しました。いくつかの望ましくない解決策を行き止まりにして、他の人がそれを試さないようにしました。この中に、x1> x2が一度計算されることを保証しようとする最初の解決策がありました。最適化後、他の単純なバージョンよりも高速ではありません。

この研究の私自身のアプリケーションは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/

3
eyepatch

ここで挿入ソートがかなり競争力がある場合、シェルソートを試すことをお勧めします。私は、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)。シェルソートおよびソートネットワーク(コンピューターサイエンスの傑出した学位論文)。花輪。 ISBN 0-824-04406-1

3
gcp

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

アルゴリズムsort6は、アルゴリズムsort4を使用するアルゴリズムsort3を使用します。軽いC++形式の実装を次に示します(オリジナルはテンプレートが多いため、ランダムアクセスイテレーターや適切な比較関数で動作します)。

つの値の並べ替え

次のアルゴリズムは、展開された挿入ソートです。 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を超えるとは考えていません。

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

EDIT:提供されたベンチマークにそれを入れた後、それはほとんどの興味深い選択肢より明らかに遅いです。それは、展開された挿入ソートよりも少し良くなる傾向がありますが、それはほとんどそれです。基本的に、これは整数に最適なソートではありませんが、比較演算が高価な型には興味深い可能性があります。

2
Morwenn

これは古い質問です。

しかし、共有したい別の種類のソリューションを作成しました。
ネストされた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に触発されたランク順序ソリューション、上記の混乱よりもはるかに高速

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
PrincePolka

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

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

パイプラインを空にすることについてはあまり心配しません(現在のx86を想定):分岐予測は長い道のりを歩んできました。私が心配するのは、コードとデータがそれぞれ1つのキャッシュラインに収まることを確認することです(おそらくコード用に2つ)。フェッチレイテンシが更新されると、ストールが補正されます。また、内部ループはおそらく10命令程度になることを意味します(ソートアルゴリズムには2つの異なる内部ループがあり、それぞれ10命令/ 22バイトと9/22の長さです)。コードにdivが含まれていないと仮定すると、それは目を見張るほど高速であると確信できます。

1
Olof Forshell

少なくとも私のシステムでは、以下で定義されている関数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 ++の特定の保証が得られると思われますが、そうでない場合はg ++がソートコードを最適化できるようになります(私は正しく覚えています、また、partは、std::sort()などの多くのstdアルゴリズムが一般にそのような卑劣な良いパフォーマンスを持っている理由の一部です。ただし、タイミングの間に、並べ替え関数の呼び出しが行われたコンテキスト(つまり、周囲のコード)がパフォーマンスに大きな影響を与えていることに気付きました。これは、関数がインライン化されて最適化されたためと思われます。たとえば、プログラムが十分に単純な場合、通常、ソート関数にポインターを渡すこととイテレーターに渡すことのパフォーマンスに大きな違いはありません。そうでない場合、通常、イテレータを使用するとパフォーマンスが著しく向上し、(少なくともこれまでの私の経験では)パフォーマンスが著しく低下することはありませんでした。これは、g ++が十分に単純なコードをグローバルに最適化できるためと思われます。

さらに、sort6_iterator()は、sometimes(再び、関数が呼び出されるコンテキストに応じて)常にデータをコピーする次のソート関数よりも優れていますソートする前にローカル変数に:

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()を次のように定義するとsometimesになりますが、パフォーマンスはわずかに向上しますが、ほとんどの場合、パフォーマンスがわずかに低下するか、パフォーマンスにわずかな差が生じます。

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

Gcc -O3が常に最適化に優れているソートアルゴリズムが必要な場合は、入力を渡す方法に応じて、次の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キーワードを使用しても、テンプレート関数は一般にVanilla C関数よりもgccにより積極的に最適化されるためです(これはgccがVanilla C関数の関数ポインターを処理する必要がありますが、テンプレート関数は処理しません) 。

1
Matthew K.

「ソート済みリストのマージ」ソートを試してください。 :) 2つの配列を使用します。大小のアレイで最速。
連結する場合、挿入する場所のみをチェックします。あなたが比較する必要のない他の大きな値(cmp = a-b> 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
peter

たぶん私はamパーティーに遅れましたが、少なくとも私の貢献はnewアプローチです。

  • コード実際はインライン化する必要があります
  • インライン化されていても、ブランチが多すぎます
  • 分析部分は基本的にO(N(N-1))で、N = 6の場合は問題ないと思われます
  • swapのコストがより高い場合(compareのコストを無効にする場合)、コードはより効果的です。
  • 静的関数がインライン化されることを信頼しています。
  • このメソッドは、rank-sort に関連しています。
    • ランクの代わりに、相対ランク(オフセット)が使用されます。
    • 順列グループのすべてのcycleのランクの合計はゼロです。
    • SWAP()ingの2つの要素の代わりに、1つのtempと1つの(register-> register)スワップ(new <-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);
}
0
wildplasser
//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
}
0
FrantzelasG

使用法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;
}
0
peter