2


2

除算では何が速くなりますか? doubles / floats / UInt32 / UInt64? C ++ / Cで

数値の乗算または除算を行うときに、何が最速かを把握するために速度テストを行いました。 オプティマイザーを倒すために本当に一生懸命努力しなければなりませんでした。 2マイクロ秒で動作する大規模なループや、乗算と除算の速度が同じである場合など、無意味な結果が得られました。

コンパイラーの最適化を十分に打ち負かすのに十分な努力をしましたが、速度を最適化しながら、これらの速度の結果を得ました。 彼らはおそらく他の誰かに興味がありますか?

私のテストがまだ失敗している場合、私に知らせてください、しかし、私がこのがらくたを書くのに2時間を費やすので、親切に見てください

64 time: 3826718 us
32 time: 2476484 us
D(mul) time: 936524 us
D(div) time: 3614857 us
S time: 1506020 us

doubleを使用した「除算の乗算」は、整数除算が続く除算を行う最も速い方法のようです。 除算の精度はテストしませんでした。 「適切な分割」がより正確である可能性がありますか? 私はこれらの速度テストの結果を見つけたいとは思っていません。ベース10定数で整数除算を使用し、コンパイラーに最適化させます;)(そして最適化も無効にしません)。

結果を取得するために使用したコードは次のとおりです。

#include

int Run(int bla, int div, int add, int minus) {
    // these parameters are to force the compiler to not be able to optimise away the
    // multiplications and divides :)
    long LoopMax = 100000000;

    uint32_t Origbla32 = 1000000000;
    long i = 0;

    uint32_t bla32 = Origbla32;
    uint32_t div32 = div;
    clock_t Time32 = clock();
    for (i = 0; i < LoopMax; i++) {
        div32 += add;
        div32 -= minus;
        bla32 = bla32 / div32;
        bla32 += bla;
        bla32 = bla32 * div32;
    }
    Time32 = clock() - Time32;

    uint64_t bla64 = bla32;
    clock_t Time64 = clock();
    uint64_t div64 = div;
    for (long i = 0; i < LoopMax; i++) {
        div64 += add;
        div64 -= minus;
        bla64 = bla64 / div64;
        bla64 += bla;
        bla64 = bla64 * div64;
    }
    Time64 = clock() - Time64;

    double blaDMul = Origbla32;
    double multodiv = 1.0 / (double)div;
    double multomul = div;
    clock_t TimeDMul = clock();
    for (i = 0; i < LoopMax; i++) {
        multodiv += add;
        multomul -= minus;
        blaDMul = blaDMul * multodiv;
        blaDMul += bla;
        blaDMul = blaDMul * multomul;
    }
    TimeDMul = clock() - TimeDMul;

    double blaDDiv = Origbla32;
    clock_t TimeDDiv = clock();
    for (i = 0; i < LoopMax; i++) {
        multodiv += add;
        multomul -= minus;
        blaDDiv = blaDDiv / multomul;
        blaDDiv += bla;
        blaDDiv = blaDDiv / multodiv;
    }
    TimeDDiv = clock() - TimeDDiv;

    float blaS = Origbla32;
    float divS = div;
    clock_t TimeS = clock();
    for (i = 0; i < LoopMax; i++) {
        divS += add;
        divS -= minus;
        blaS = blaS / divS;
        blaS += bla;
        blaS = blaS * divS;
    }
    TimeS = clock() - TimeS;

    printf("64 time: %i us  (%i)\n", (int)Time64, (int)bla64);
    printf("32 time: %i us  (%i)\n", (int)Time32, bla32);

    printf("D(mul) time: %i us  (%f)\n", (int)TimeDMul, blaDMul);
    printf("D(div) time: %i us  (%f)\n", (int)TimeDDiv, blaDDiv);
    printf("S time: %i us  (%f)\n", (int)TimeS, blaS);

    return 0;
}

int main(int argc, char* const argv[]) {
    Run(0, 10, 0, 0); // adds and minuses 0 so it doesn't affect the math, only kills the opts
    return 0;
}

5 Answer


9


特定の算術演算を実行する方法は多数あるため、単一の答え(シフト、分数乗算、実際の除算、対数単位でのラウンドトリップなど)がない場合があります。これらはすべて、オペランドと資源配分)。

コンパイラに、プログラムとデータフローの情報を処理させます。

x86でのアセンブリに適用可能なデータについては、http://gmplib.org/~tege/x86-timing.pdf ["AMDおよびIntel x86プロセッサの命令レイテンシとスループット"]を参照してください。


4


最も速いものは、ターゲットアーキテクチャに完全に依存します。 あなたがたまたまあるプラットフォームにのみ興味があるように見えますが、実行時間から推測すると、Intel(Core2?)またはAMDの64ビットx86のようです。

とはいえ、逆数による浮動小数点乗算は多くのプラットフォームで最速になりますが、推測されるように、通常は浮動小数点除算よりも精度が低くなります(1つではなく2つの丸め-使用法に関係なく)別の質問です)。 一般に、可能な限り効率的に除算を行うためにフープをジャンプするよりも少ない除算を使用するようにアルゴリズムを再配置し、最速の除算は実行しないものにすることをお勧めします。分割のボトルネックとなるアルゴリズムはほとんどないため、最適化にまったく時間をかけます。

また、整数ソースがあり、整数の結果が必要な場合は、ベンチマークに整数と浮動小数点間の変換のコストを含めるようにしてください。

特定のマシンでのタイミングに関心があるため、Intelはhttp://www.intel.com/Assets/PDF/manual/248966.pdf[Optimization Reference Manual(pdf)でこの情報を公開しています。 ]。 具体的には、付録Cのセクション3.1「レジスタオペランドでのレイテンシとスループット」の表に関心があります。

整数除算のタイミングは、実際の値に大きく依存することに注意してください。 そのガイドの情報に基づいて、測定したパフォーマンス比がIntelの公開情報と一致しないため、タイミングルーチンにはまだかなりのオーバーヘッドがあるようです。


2


Stephenが述べたように、http://www.intel.com/Assets/PDF/manual/248966.pdf [最適化マニュアル]を使用しますが、SSE命令の使用も検討する必要があります。 これらは、1つの命令で4または8の除算/乗算を実行できます。

また、部門が処理するのに1クロックサイクルかかることはかなり一般的です。 結果はいくつかのクロックサイクル(レイテンシと呼ばれます)の間利用できない場合がありますが、最初の結果を必要としない限り、次の分割はこの時間中に開始できます(最初と重複)。 これは、前の負荷がまだ乾燥している間により多くの衣服を洗うことができるのと同じように、CPUのパイプライン処理によるものです。

乗算による除算は一般的なトリックであり、除数が頻繁に変更されない場所で使用する必要があります。

最終的な実装を制限するのは、(入力をナビゲートして出力を書き込む際の)メモリアクセスの速度であることを発見するためだけに、数学を高速化するために時間と労力を費やす可能性が非常に高いです。


0


MSVC 2008でこれを行うための欠陥テストを作成しました

double i32Time  = GetTime();
{
    volatile __int32 i = 4;
    __int32 count   = 0;
    __int32 max     = 1000000;
    while( count < max )
    {
        i /= 61;
        count++;
    }
}
i32Time = GetTime() - i32Time;

double i64Time  = GetTime();
{
    volatile __int64 i = 4;
    __int32 count   = 0;
    __int32 max     = 1000000;
    while( count < max )
    {
        i /= 61;
        count++;
    }
}
i64Time = GetTime() - i64Time;


double fTime    = GetTime();
{
    volatile float i = 4;
    __int32 count   = 0;
    __int32 max     = 1000000;
    while( count < max )
    {
        i /= 4.0f;
        count++;
    }
}
fTime   = GetTime() - fTime;

double fmTime   = GetTime();
{
    volatile float i = 4;
    const float div = 1.0f / 4.0f;
    __int32 count   = 0;
    __int32 max     = 1000000;
    while( count < max )
    {
        i *= div;
        count++;
    }
}
fmTime  = GetTime() - fmTime;

double dTime    = GetTime();
{
    volatile double i = 4;
    __int32 count   = 0;
    __int32 max     = 1000000;
    while( count < max )
    {
        i /= 4.0f;
        count++;
    }
}
dTime   = GetTime() - dTime;

double dmTime   = GetTime();
{
    volatile double i = 4;
    const double div = 1.0f / 4.0f;
    __int32 count   = 0;
    __int32 max     = 1000000;
    while( count < max )
    {
        i *= div;
        count++;
    }
}
dmTime  = GetTime() - dmTime;


DebugOutput( _T( "%f\n" ), i32Time );
DebugOutput( _T( "%f\n" ), i64Time );
DebugOutput( _T( "%f\n" ), fTime );
DebugOutput( _T( "%f\n" ), fmTime );
DebugOutput( _T( "%f\n" ), dTime );
DebugOutput( _T( "%f\n" ), dmTime );

DebugBreak();

次に、32ビットモードのAMD64 Turion 64で実行しました。 私が得た結果は次のとおりです。

0.006622
0.054654
0.006283
0.006353
0.006203
0.006161

テストに欠陥がある理由は、volatileが使用されているために、コンパイラが変数が変更された場合に備えてメモリから変数を再ロードするように強制するためです。 その中のすべては、このマシンの実装のどれにもほとんど貴重な違いがないことを示しています(__int64は明らかに遅いです)。

また、MSVCコンパイラーが相互最適化による乗算を実行することを明確に示しています。 GCCの方が良くないとしても同じことをしていると思います。 「i」で除算するようにフロートおよび二重除算チェックを変更すると、時間が大幅に増加します。 ただし、その多くはディスクからの再ロードである可能性がありますが、コンパイラがそれを簡単に最適化できないことは明らかです。

そのようなマイクロ最適化を理解するには、http://www.linux-kongress.org/2009/slides/compiler_survey_felix_von_leitner.pdf [このpdf]を読んでみてください。

そうしたことを心配しているのであれば、明らかにコードのプロファイルを作成していないと私は主張します。 問題が実際に問題になったときに、問題をプロファイリングして修正します。


0


Agner Fogは自分でかなり詳細な測定をいくつか行っています。これはhttp://www.agner.org/optimize/instruction_tables.pdf [こちら]で確認できます。 本当に最適化しようとしている場合は、彼のhttp://www.agner.org/optimize/ [ソフトウェア最適化リソース]からも残りのドキュメントを読む必要があります。

ベクトル化されていない浮動小数点演算を測定している場合でも、コンパイラには生成されたアセンブリに対して2つのオプションがあります。FPU命令( fadd、` fmul`)を使用できるか、SSE命令を使用できます命令ごとに1つの浮動小数点値を操作します( addss、` mulss`)。 私の経験では、SSE命令はより高速で不正確ではありませんが、コンパイラーは古い動作に依存するコードとの互換性を損なう可能性があるため、これをデフォルトにしません。 `-mfpmath = sse`フラグでgccで有効にできます。