このトピックを見つけたソートされた配列を処理する方がソートされていない配列を処理するよりも速いのはなぜですか?。そして、このコードを実行しようとします。そして、奇妙な動作が見つかります。このコードを-O3
最適化フラグ付きでコンパイルすると2.98605 sec
、実行に かかります。-O2
付きでコンパイルすると かかり1.98093 sec
ます。同じ環境の同じマシンでこのコードを複数回 (5 回または 6 回) 実行し、他のすべてのソフトウェア (Chrome、Skype など) を閉じます。
gcc --version
gcc (Ubuntu 4.9.2-0ubuntu1~14.04) 4.9.2
Copyright (C) 2014 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
それで、なぜこのようなことが起こるのか説明していただけますか? マニュアルを読んで、 が含まれているgcc
ことがわかりました。 ご協力ありがとうございます。-O3
-O2
追伸コードを追加
#include <algorithm>
#include <ctime>
#include <iostream>
int main()
{
// Generate data
const unsigned arraySize = 32768;
int data[arraySize];
for (unsigned c = 0; c < arraySize; ++c)
data[c] = std::rand() % 256;
// !!! With this, the next loop runs faster
std::sort(data, data + arraySize);
// Test
clock_t start = clock();
long long sum = 0;
for (unsigned i = 0; i < 100000; ++i)
{
// Primary loop
for (unsigned c = 0; c < arraySize; ++c)
{
if (data[c] >= 128)
sum += data[c];
}
}
double elapsedTime = static_cast<double>(clock() - start) / CLOCKS_PER_SEC;
std::cout << elapsedTime << std::endl;
std::cout << "sum = " << sum << std::endl;
}
ベストアンサー1
gcc -O3
使用シーモブ条件文では、ループで運ばれる依存関係チェーンが長くなり、cmov
(Intel Sandybridge CPUでは2つのuopと2サイクルのレイテンシになります。アグナー・フォグの指示表. も参照してください86 のタグウィキ)。これはcmov
最悪なケースの一つ。
データが多少なりとも予測不可能であれば、cmov
おそらく勝利となるでしょう。したがって、これはコンパイラにとってかなり賢明な選択です。(ただし、コンパイラは分岐のないコードを多用することがある。
私Godboltコンパイラエクスプローラーにコードを入れるasm を表示します (適切に強調表示され、無関係な行がフィルタリングされます。ただし、main() に到達するには、すべてのソート コードを下にスクロールする必要があります)。
.L82: # the inner loop from gcc -O3
movsx rcx, DWORD PTR [rdx] # sign-extending load of data[c]
mov rsi, rcx
add rcx, rbx # rcx = sum+data[c]
cmp esi, 127
cmovg rbx, rcx # sum = data[c]>127 ? rcx : sum
add rdx, 4 # pointer-increment
cmp r12, rdx
jne .L82
gcc は ADD の代わりに LEA を使用して MOV を保存できたはずです。
ループの 1 回の反復で CMO を使用して rbx が書き込まれ、次の反復で ADD を使用して rbx が読み取られるため、ループは ADD->CMOV (3 サイクル) のレイテンシでボトルネックになります。
ループには 8 つの融合ドメイン uop のみが含まれているため、2 サイクルごとに 1 つの発行が可能です。実行ポートの負荷も、sum
依存関係チェーンのレイテンシほどボトルネックにはなりませんが、それに近いものです (Sandybridge には 3 つの ALU ポートしかありませんが、Haswell には 4 つあります)。
sum += (data[c] >= 128 ? data[c] : 0);
ところで、ループで運ばれる依存関係チェーンからを取り出すように書くと、cmov
潜在的に便利です。まだ命令はたくさんありますが、cmov
各反復の は独立しています。これは-O2
gcc6.3以前では期待通りにコンパイルされますcmov
ですが、gcc7 はクリティカルパス上で に最適化を解除します(https://gcc.gnu.org/bugzilla/show_bug.cgi?id=82666if()
)。(また、この書き方よりも古いバージョンの gcc でも自動ベクトル化されます。)
Clang は、元のソースであっても、cmov をクリティカル パスから外します。
gcc -O2
分岐を使用します (gcc5.x 以前)。これは、データがソートされているため、予測が正確です。最近の CPU は分岐予測を使用して制御依存関係を処理するため、ループで運ばれる依存関係チェーンは短くなりますadd
(レイテンシは 1 サイクルのみ)。
分岐予測 + 投機的実行により、各反復の比較と分岐は独立しており、分岐方向が確実にわかる前に実行を続行できます。
.L83: # The inner loop from gcc -O2
movsx rcx, DWORD PTR [rdx] # load with sign-extension from int32 to int64
cmp ecx, 127
jle .L82 # conditional-jump over the next instruction
add rbp, rcx # sum+=data[c]
.L82:
add rdx, 4
cmp rbx, rdx
jne .L83
ループによって運ばれる依存関係チェーンが 2 つあります。sum
ループ カウンターsum
は 0 または 1 サイクルの長さで、ループ カウンターは常に 1 サイクルの長さです。ただし、ループは Sandybridge では 5 つの融合ドメイン uop であるため、いずれにしても反復ごとに 1c で実行することはできないため、レイテンシはボトルネックにはなりません。
これはおそらく、2 サイクルあたり約 1 回の反復で実行されます (分岐命令のスループットがボトルネック)。これに対して、-O3 ループでは 3 サイクルあたり 1 回です。次のボトルネックは、ALU uop のスループットです。ALU uop は 4 つ (分岐しない場合) ですが、ALU ポートは 3 つだけです (ADD はどのポートでも実行できます)。
このパイプライン分析の予測は、-O3 の場合は約 3 秒、-O2 の場合は約 2 秒というタイミングとほぼ正確に一致しています。
Haswell/Skylakeは、分岐しない分岐を分岐する分岐と同じサイクルで実行でき、4つのALUポートを備えているため、分岐しないケースを1.25サイクルに1回実行できます。(または、5 uop ループは毎サイクル 4 uop で発行されるわけではない)。
(テストしたところ、Skylake @ 3.9GHz では、プログラム全体のブランチバージョンを 1.45 秒で実行し、ブランチレスバージョンを 1.68 秒で実行しました。そのため、その差ははるかに小さくなります。)
g++6.3.1cmov
は でも を使用します-O2
が、g++5.4 は依然として 4.9.2 のように動作します。
g++6.3.1 と g++5.4 の両方で、-fprofile-generate
/ を使用すると、 ( の場合)-fprofile-use
でも分岐バージョンが生成されます。-O3
-fno-tree-vectorize
新しい gcc のループの CMOV バージョンでは、 CMP/CMOV の代わりにadd ecx,-128
/ を使用しますcmovge rbx,rdx
。これは少し奇妙ですが、おそらく速度は低下しません。ADD はフラグだけでなく出力レジスタにも書き込むため、物理レジスタの数にさらに負担がかかります。ただし、それがボトルネックにならない限り、ほぼ同じになるはずです。
新しい gcc は -O3 でループを自動ベクトル化します。これにより、SSE2 だけでも大幅な高速化が実現します。(たとえば、私の i7-6700k Skylake ではベクトル化されたバージョンを 0.74 秒で実行します。つまり、スカラーの約 2 倍の速度です。または-O3 -march=native
、AVX2 256b ベクトルを使用すると 0.35 秒です)。
ベクトル化されたバージョンは命令がたくさんあるように見えますが、それほど悪くはなく、そのほとんどはループで運ばれる依存関係チェーンの一部ではありません。最後のほうで 64 ビット要素に展開するだけで済みます。pcmpgtd
ただし、条件によってすべての負の整数がすでにゼロになっている場合、符号拡張ではなくゼロ拡張だけで済むことに気付かないため、2 回実行されます。