web-dev-qa-db-ja.com

なぜpow(int、int)がそんなに遅いのですか?

私は、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)の明らかな実装の詳細はありませんか?

48
Fang

pow()は実際の浮動小数点数で動作し、フードの下で式を使用します

_pow(x,y) = e^(y log(x))
_

_x^y_を計算します。 intは、doubleを呼び出す前にpowに変換されます。 (logは自然対数、eベース)

したがって、pow()を使用した_x^2_は、_x*x_よりも低速です。

関連するコメントに基づいて編集する

  • 整数の指数でpowを使用すると、誤った結果が生じる場合があります(PaulMcKenzie
  • double typeで数学関数を使用することに加えて、powは関数呼び出しです(_x*x_はそうではありません)(-jtbandes
  • 実際、多くの最新のコンパイラは定数整数引数を使用してpowを最適化しますが、これに依存するべきではありません。
69
Ring Ø

チェックする最も遅い方法の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も参照してください。

38
Peter Cordes