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サイクルになります。私はそれを驚くほど速く呼びます。他に可能な改善はありますか?
__asm__ volatile (".byte 0x0f, 0x31; shlq $32, %%rdx; orq %%rdx, %0" : "=a" (x) : : "rdx");
、GCCが単一の64ビットレジスタに期待するのに対して、rdtscがEDX:EAXに回答を置くためです。-O3でコンパイルすると、バグを確認できます。より速いSWAPについてのPaul Rへの私のコメントも以下を参照してください。
CMP EAX, EBX; SBB EAX, EAX
0または0xFFFFFFFFを入れます。 (「キャリー付き加算」)に対応する「借用付き減算」です。ステータスは、参照ビットでキャリービット。繰り返しになりますが、Pentium 4 とでのレイテンシとスループットはひどいものでしたが、Core CPUでは2倍の速度でした。80386以降、条件付きストアと条件付き移動の命令もありますが、速度も遅いです。EAX
EAX
EBX
SBB
ADC
ADC
SBB
ADD
SUB
SETcc
CMOVcc
x-y
でx+y
アンダーフローやオーバーフローが発生しないと想定できますか?