私は、C++の知識を向上させるために、オイラー演習のいくつかのプロジェクトに取り組んできました。
次の関数を作成しました。
_int a = 0,b = 0,c = 0;
for (a = 1; a <= SUMTOTAL; a++)
{
for (b = a+1; b <= SUMTOTAL-a; b++)
{
c = SUMTOTAL-(a+b);
if (c == sqrt(pow(a,2)+pow(b,2)) && b < c)
{
std::cout << "a: " << a << " b: " << b << " c: "<< c << std::endl;
std::cout << a * b * c << std::endl;
}
}
}
_
これは17ミリ秒で計算されます。
ただし、行を変更すると
_if (c == sqrt(pow(a,2)+pow(b,2)) && b < c)
_
に
_if (c == sqrt((a*a)+(b*b)) && b < c)
_
計算は2ミリ秒で行われます。最初の式の計算が非常に遅くなるpow(int, int)
の明らかな実装の詳細はありませんか?
pow()
は実際の浮動小数点数で動作し、フードの下で式を使用します
_pow(x,y) = e^(y log(x))
_
_x^y
_を計算します。 int
は、double
を呼び出す前にpow
に変換されます。 (log
は自然対数、eベース)
したがって、pow()
を使用した_x^2
_は、_x*x
_よりも低速です。
関連するコメントに基づいて編集する
pow
を使用すると、誤った結果が生じる場合があります(PaulMcKenzie)pow
は関数呼び出しです(_x*x
_はそうではありません)(-jtbandes)チェックする最も遅い方法の1つを選択しました
_c*c == a*a + b*b // assuming c is non-negative
_
これは3つの整数の乗算にコンパイルされます(そのうちの1つはループから引き上げられます)。 pow()
がなくても、double
に変換し、平方根を取得しています。これはスループットにとってはひどいものです。 (そしてレイテンシーもありますが、最新のCPUでの分岐予測+投機的実行は、レイテンシーがここでの要因ではないことを意味します)。
Intel HaswellのSQRTSD命令のスループットは8〜14サイクルに1つです( ソース:Agner Fogの命令テーブル )。したがって、sqrt()
バージョンがFP sqrt実行ユニットが飽和状態になりましたが、gccが発行する速度(以下)よりも約4倍遅いです。
条件の_b < c
_部分がfalseになったときにループから抜け出すようにループ条件を最適化することもできるため、コンパイラーはそのチェックの1つのバージョンを実行するだけで済みます。
_void foo_optimized()
{
for (int a = 1; a <= SUMTOTAL; a++) {
for (int b = a+1; b < SUMTOTAL-a-b; b++) {
// int c = SUMTOTAL-(a+b); // gcc won't always transform signed-integer math, so this prevents hoisting (SUMTOTAL-a) :(
int c = (SUMTOTAL-a) - b;
// if (b >= c) break; // just changed the loop condition instead
// the compiler can hoist a*a out of the loop for us
if (/* b < c && */ c*c == a*a + b*b) {
// Just print a newline. std::endl also flushes, which bloats the asm
std::cout << "a: " << a << " b: " << b << " c: "<< c << '\n';
std::cout << a * b * c << '\n';
}
}
}
}
_
これは、(gcc6.2 _-O3 -mtune=haswell
_で)コンパイルして、この内部ループでコード化します。 Godbolt compiler Explorer の完全なコードを参照してください。
_# a*a is hoisted out of the loop. It's in r15d
.L6:
add ebp, 1 # b++
sub ebx, 1 # c--
add r12d, r14d # ivtmp.36, ivtmp.43 # not sure what this is or why it's in the loop, would have to look again at the asm outside
cmp ebp, ebx # b, _39
jg .L13 ## This is the loop-exit branch, not-taken until the end
## .L13 is the rest of the outer loop.
## It sets up for the next entry to this inner loop.
.L8:
mov eax, ebp # multiply a copy of the counters
mov edx, ebx
imul eax, ebp # b*b
imul edx, ebx # c*c
add eax, r15d # a*a + b*b
cmp edx, eax # tmp137, tmp139
jne .L6
## Fall-through into the cout print code when we find a match
## extremely rare, so should predict near-perfectly
_
Intel Haswellでは、これらの指示はすべて1 uopです。 (そして、cmp/jccは、マクロヒューズを比較と分岐のuopにペアにします。)これは、10個の融合ドメインuopです 2.5サイクルごとに1回の反復で発行可能 。
Haswellは、クロックあたり1反復のスループットで_imul r32, r32
_を実行するため、内部ループ内の2つの乗算は、2.5cあたり2つの乗算でポート1を飽和させません。これにより、ポート1を盗むADDおよびSUBからの避けられないリソースの競合を吸収する余地が残ります。
私たちは他の実行ポートのボトルネックにさえ近づいていないので、フロントエンドのボトルネックが唯一の問題であり、これは2.5サイクルごとに1回の繰り返しで実行する必要がありますIntel Haswell以降。
ループ展開は、ここでチェックごとのuopの数を減らすのに役立ちます。例えば_lea ecx, [rbx+1]
_を使用して次の反復でb + 1を計算するため、MOVを使用せずに_imul ebx, ebx
_を使用して非破壊にすることができます。
強度の低減も可能です:_b*b
_が与えられた場合、IMULなしで_(b-1) * (b-1)
_を計算できます。 _(b-1) * (b-1) = b*b - 2*b + 1
_。したがって、多分、_lea ecx, [rbx*2 - 1]
_を実行し、それを_b*b
_から差し引くことができます。 (加算の代わりに減算するアドレッシングモードはありません。たぶん、レジスタに_-b
_を保持し、ゼロにカウントアップできるので、_lea ecx, [rcx + rbx*2 - 1]
_を使用して_b*b
_を更新できます。 ECXでは、EBXで_-b
_が指定されます)。
実際にIMULスループットのボトルネックにならない限り、これはより多くのuopを必要とし、勝つことはできません。コンパイラーがC++ソースのこの強度低下をどの程度うまく処理できるかを見るのは楽しいかもしれません。
おそらく、これをSSEまたはAVX)でベクトル化し、4つまたは8つの連続したb
値をチェックすることもできます。ヒットは非常にまれなので、8のいずれかがヒットしたかどうかをチェックし、まれなケースで一致したものを選別します。
最適化の詳細については、 x86 タグwikiも参照してください。