0.1fを0に変更すると、パフォーマンスが10倍遅くなるのはなぜですか?


1527

なぜこのようなコードは、

const float x[16] = {  1.1,   1.2,   1.3,     1.4,   1.5,   1.6,   1.7,   1.8,
                       1.9,   2.0,   2.1,     2.2,   2.3,   2.4,   2.5,   2.6};
const float z[16] = {1.123, 1.234, 1.345, 156.467, 1.578, 1.689, 1.790, 1.812,
                     1.923, 2.034, 2.145,   2.256, 2.367, 2.478, 2.589, 2.690};
float y[16];
for (int i = 0; i < 16; i++)
{
    y[i] = x[i];
}

for (int j = 0; j < 9000000; j++)
{
    for (int i = 0; i < 16; i++)
    {
        y[i] *= x[i];
        y[i] /= z[i];
        y[i] = y[i] + 0.1f; // <--
        y[i] = y[i] - 0.1f; // <--
    }
}

次のビットよりも10倍以上高速に実行されますか?

const float x[16] = {  1.1,   1.2,   1.3,     1.4,   1.5,   1.6,   1.7,   1.8,
                       1.9,   2.0,   2.1,     2.2,   2.3,   2.4,   2.5,   2.6};
const float z[16] = {1.123, 1.234, 1.345, 156.467, 1.578, 1.689, 1.790, 1.812,
                     1.923, 2.034, 2.145,   2.256, 2.367, 2.478, 2.589, 2.690};
float y[16];
for (int i = 0; i < 16; i++)
{
    y[i] = x[i];
}

for (int j = 0; j < 9000000; j++)
{
    for (int i = 0; i < 16; i++)
    {
        y[i] *= x[i];
        y[i] /= z[i];
        y[i] = y[i] + 0; // <--
        y[i] = y[i] - 0; // <--
    }
}

Visual Studio 2010 SP1でコンパイルする場合。最適化レベルだった-02sse2有効。私は他のコンパイラでテストしていません。


10
違いをどのように測定しましたか?また、コンパイル時にどのオプションを使用しましたか?
James Kanze

158
この場合、コンパイラが+/- 0をドロップしないのはなぜですか?
マイケルドーガン、2012

127
@ Zyx2000コンパイラはその愚かな近くにありません。それはあなたが使用するかどうか、同じコードを吐き出すことLINQPadショーでの簡単な例を分解00f0d、あるいは(int)0文脈でdouble必要とされています。
ミリムース2012

14
最適化レベルとは何ですか?
Otto Allmendinger 2012

回答:


1616

非正規化浮動小数点の世界へようこそ!彼らはパフォーマンスに大混乱をもたらす可能性があります!!!

非正規(または非正規)数値は、浮動小数点表現からゼロに非常に近いいくつかの追加の値を取得するための一種のハックです。非正規化浮動小数点の演算は、正規化浮動小数点の演算より数十倍から数百倍遅くなる可能性があります。これは、多くのプロセッサがそれらを直接処理できず、マイクロコードを使用してそれらをトラップして解決する必要があるためです。

10,000回の反復後に数値を出力すると、0または0.1が使用されているかどうかに応じて異なる値に収束していることがわかります。

以下は、x64でコンパイルされたテストコードです。

int main() {

    double start = omp_get_wtime();

    const float x[16]={1.1,1.2,1.3,1.4,1.5,1.6,1.7,1.8,1.9,2.0,2.1,2.2,2.3,2.4,2.5,2.6};
    const float z[16]={1.123,1.234,1.345,156.467,1.578,1.689,1.790,1.812,1.923,2.034,2.145,2.256,2.367,2.478,2.589,2.690};
    float y[16];
    for(int i=0;i<16;i++)
    {
        y[i]=x[i];
    }
    for(int j=0;j<9000000;j++)
    {
        for(int i=0;i<16;i++)
        {
            y[i]*=x[i];
            y[i]/=z[i];
#ifdef FLOATING
            y[i]=y[i]+0.1f;
            y[i]=y[i]-0.1f;
#else
            y[i]=y[i]+0;
            y[i]=y[i]-0;
#endif

            if (j > 10000)
                cout << y[i] << "  ";
        }
        if (j > 10000)
            cout << endl;
    }

    double end = omp_get_wtime();
    cout << end - start << endl;

    system("pause");
    return 0;
}

出力:

#define FLOATING
1.78814e-007  1.3411e-007  1.04308e-007  0  7.45058e-008  6.70552e-008  6.70552e-008  5.58794e-007  3.05474e-007  2.16067e-007  1.71363e-007  1.49012e-007  1.2666e-007  1.11759e-007  1.04308e-007  1.04308e-007
1.78814e-007  1.3411e-007  1.04308e-007  0  7.45058e-008  6.70552e-008  6.70552e-008  5.58794e-007  3.05474e-007  2.16067e-007  1.71363e-007  1.49012e-007  1.2666e-007  1.11759e-007  1.04308e-007  1.04308e-007

//#define FLOATING
6.30584e-044  3.92364e-044  3.08286e-044  0  1.82169e-044  1.54143e-044  2.10195e-044  2.46842e-029  7.56701e-044  4.06377e-044  3.92364e-044  3.22299e-044  3.08286e-044  2.66247e-044  2.66247e-044  2.24208e-044
6.30584e-044  3.92364e-044  3.08286e-044  0  1.82169e-044  1.54143e-044  2.10195e-044  2.45208e-029  7.56701e-044  4.06377e-044  3.92364e-044  3.22299e-044  3.08286e-044  2.66247e-044  2.66247e-044  2.24208e-044

2回目の実行で、数値がゼロに非常に近いことに注意してください。

非正規化された数値は一般的にはまれであるため、ほとんどのプロセッサはそれらを効率的に処理しようとしません。


コードの先頭にこれを追加して非正規化数をゼロにフラッシュすると、これが非正規化数と関係があることを示すために、次のようにします。

_MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON);

次に、のバージョンが010倍遅くなることはなくなり、実際に速くなります。(SSEを有効にしてコードをコンパイルする必要があります。)

つまり、これらの奇妙な低い精度のほぼゼロの値を使用するのではなく、代わりにゼロに丸めるだけです。

タイミング:Core i7 920 @ 3.5 GHz:

//  Don't flush denormals to zero.
0.1f: 0.564067
0   : 26.7669

//  Flush denormals to zero.
0.1f: 0.587117
0   : 0.341406

結局のところ、これは整数か浮動小数点かに関係ありません。0または0.1f/両方のループのレジスタ外部に格納され変換されます。したがって、パフォーマンスには影響しません。


100
"+ 0"がデフォルトでコンパイラーによって完全に最適化されていないことは、少し奇妙なことです。もし彼が "+ 0.0f"を入れていたら、これは起こったでしょうか?
s73v3r 2012

51
@ s73v3rそれはとても良い質問です。今、私はアセンブリを見て、+ 0.0f最適化さえされていません。私が推測しなければならなかった場合、それが信号または何かであった+ 0.0f場合、それは副作用をもたらす可能性があります...私は間違っているかもしれませんが。y[i]NaN
ミスティック

14
Doublesは、多くの場合、数値の大きさが異なるだけで同じ問題に遭遇します。Flush-to-Zeroはオーディオアプリケーション(および1e-38をあちこちに失う余裕がある他のアプリケーション)には問題ありませんが、x87には当てはまらないと思います。FTZがない場合、オーディオアプリケーションの通常の修正は、非常に低い振幅(可聴ではない)DCまたは方形波信号を非正規性から離れたジッター数に注入することです。
Russell Borogove、2012

16
@Isaacは、y [i]が0.1を大幅に下回る場合、数値を追加すると、数値の最上位桁が高くなるため、精度が低下するためです。
DanはFirelightによって

167
@ s73v3r:浮動小数点が負の0を持ち、+ 0.fを-.0fに追加した結果が+ 0.fであるため、+ 0.fを最適化できません。したがって、0.fの追加は恒等操作ではなく、最適化することはできません。
Eric Postpischil

415

gcc生成されたアセンブリにdiffを使用して適用すると、この違いのみが生じます。

73c68,69
<   movss   LCPI1_0(%rip), %xmm1
---
>   movabsq $0, %rcx
>   cvtsi2ssq   %rcx, %xmm1
81d76
<   subss   %xmm1, %xmm0

cvtsi2ssq遅く確かに10倍である1。

明らかに、floatバージョンはメモリからロードされたXMMレジスタを使用しますが、intバージョンは実際のint値0を命令をfloat使用するcvtsi2ssqように変換しますが、多くの時間がかかります。-O3gccに渡しても役に立ちません。(gccバージョン4.2.1。)

(使用するdouble代わりにfloat問題、それは変わることを除いてはありませんcvtsi2ssqcvtsi2sdq。)

更新

いくつかの追加のテストは、それが必ずしもcvtsi2ssq指示ではないことを示しています。(a int ai=0;float a=ai;を使用し、のa代わりにを使用して0)除去すると、速度の差が残ります。したがって、@ Mysticialは正しいです。非正規化浮動小数点数が違いを生みます。これは、0との間の値をテストすることで確認できます0.1f。上記のコードのターニングポイントは、0.00000000000000000000000000000001ループが突然10倍かかるときのおよそです。

更新<< 1

この興味深い現象の小さな視覚化:

  • 列1:フロート、反復ごとに2で除算
  • 列2:このフロートのバイナリ表現
  • 列3:このフロートを1e7回合計するのにかかる時間

非正規化が始まると、指数(最後の9ビット)が最小値に変化することがはっきりとわかります。その時点で、単純な加算は20倍遅くなります。

0.000000000000000000000000000000000100000004670110: 10111100001101110010000011100000 45 ms
0.000000000000000000000000000000000050000002335055: 10111100001101110010000101100000 43 ms
0.000000000000000000000000000000000025000001167528: 10111100001101110010000001100000 43 ms
0.000000000000000000000000000000000012500000583764: 10111100001101110010000110100000 42 ms
0.000000000000000000000000000000000006250000291882: 10111100001101110010000010100000 48 ms
0.000000000000000000000000000000000003125000145941: 10111100001101110010000100100000 43 ms
0.000000000000000000000000000000000001562500072970: 10111100001101110010000000100000 42 ms
0.000000000000000000000000000000000000781250036485: 10111100001101110010000111000000 42 ms
0.000000000000000000000000000000000000390625018243: 10111100001101110010000011000000 42 ms
0.000000000000000000000000000000000000195312509121: 10111100001101110010000101000000 43 ms
0.000000000000000000000000000000000000097656254561: 10111100001101110010000001000000 42 ms
0.000000000000000000000000000000000000048828127280: 10111100001101110010000110000000 44 ms
0.000000000000000000000000000000000000024414063640: 10111100001101110010000010000000 42 ms
0.000000000000000000000000000000000000012207031820: 10111100001101110010000100000000 42 ms
0.000000000000000000000000000000000000006103515209: 01111000011011100100001000000000 789 ms
0.000000000000000000000000000000000000003051757605: 11110000110111001000010000000000 788 ms
0.000000000000000000000000000000000000001525879503: 00010001101110010000100000000000 788 ms
0.000000000000000000000000000000000000000762939751: 00100011011100100001000000000000 795 ms
0.000000000000000000000000000000000000000381469876: 01000110111001000010000000000000 896 ms
0.000000000000000000000000000000000000000190734938: 10001101110010000100000000000000 813 ms
0.000000000000000000000000000000000000000095366768: 00011011100100001000000000000000 798 ms
0.000000000000000000000000000000000000000047683384: 00110111001000010000000000000000 791 ms
0.000000000000000000000000000000000000000023841692: 01101110010000100000000000000000 802 ms
0.000000000000000000000000000000000000000011920846: 11011100100001000000000000000000 809 ms
0.000000000000000000000000000000000000000005961124: 01111001000010000000000000000000 795 ms
0.000000000000000000000000000000000000000002980562: 11110010000100000000000000000000 835 ms
0.000000000000000000000000000000000000000001490982: 00010100001000000000000000000000 864 ms
0.000000000000000000000000000000000000000000745491: 00101000010000000000000000000000 915 ms
0.000000000000000000000000000000000000000000372745: 01010000100000000000000000000000 918 ms
0.000000000000000000000000000000000000000000186373: 10100001000000000000000000000000 881 ms
0.000000000000000000000000000000000000000000092486: 01000010000000000000000000000000 857 ms
0.000000000000000000000000000000000000000000046243: 10000100000000000000000000000000 861 ms
0.000000000000000000000000000000000000000000022421: 00001000000000000000000000000000 855 ms
0.000000000000000000000000000000000000000000011210: 00010000000000000000000000000000 887 ms
0.000000000000000000000000000000000000000000005605: 00100000000000000000000000000000 799 ms
0.000000000000000000000000000000000000000000002803: 01000000000000000000000000000000 828 ms
0.000000000000000000000000000000000000000000001401: 10000000000000000000000000000000 815 ms
0.000000000000000000000000000000000000000000000000: 00000000000000000000000000000000 42 ms
0.000000000000000000000000000000000000000000000000: 00000000000000000000000000000000 42 ms
0.000000000000000000000000000000000000000000000000: 00000000000000000000000000000000 44 ms

ARMに関する同等の議論は、スタックオーバーフローの質問Objective-Cの非正規化浮動小数点にありますか?


27
-Osは修正しませんが、修正-ffast-mathします。(私はいつもそれを使用しています
。IMO

gcc-4.6では、ポジティブな最適化レベルでの変換はありません。
Jed

@leftaroundabout:-ffast-mathMXCSRでFTZ(flush to zero)とDAZ(denormal is zero)を設定する追加の起動コードをリンクして実行可能ファイル(ライブラリではない)をコンパイルするため、CPUがデノーマルに対して低速のマイクロコードアシストを実行する必要がありません。
Peter Cordes

34

非正規化された浮動小数点の使用が原因です。それとパフォーマンスペナルティの両方を取り除く方法は?非正規数を殺す方法についてインターネットを精査してきたため、これを行う「最善の」方法はまだないようです。異なる環境で最適に動作する可能性のあるこれら3つの方法を見つけました。

  • 一部のGCC環境では機能しない可能性があります。

    // Requires #include <fenv.h>
    fesetenv(FE_DFL_DISABLE_SSE_DENORMS_ENV);
  • 一部のVisual Studio環境では機能しない場合があります:1

    // Requires #include <xmmintrin.h>
    _mm_setcsr( _mm_getcsr() | (1<<15) | (1<<6) );
    // Does both FTZ and DAZ bits. You can also use just hex value 0x8040 to do both.
    // You might also want to use the underflow mask (1<<11)
  • GCCとVisual Studioの両方で機能するように見えます。

    // Requires #include <xmmintrin.h>
    // Requires #include <pmmintrin.h>
    _MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON);
    _MM_SET_DENORMALS_ZERO_MODE(_MM_DENORMALS_ZERO_ON);
  • Intelコンパイラには、最新のIntel CPUでデフォルトで非正規化を無効にするオプションがあります。詳細はこちら

  • コンパイラスイッチ。または-ffast-math、デノーマルを無効にして他のいくつかのことを速くしますが、残念ながら、コードを壊す可能性のある他の多くの近似も行います。慎重にテストしてください!Visual Studioコンパイラーのfast-mathに相当するものですが、これも非正規化を無効にするかどうかを確認できませんでした。1-msse-mfpmath=sse/fp:fast


1
これは、異なるが関連する質問に対する適切な答えのように聞こえます(数値計算で非正規の結果が生成されるのを防ぐにはどうすればよいですか?)ただし、この質問には答えません。
Ben Voigt 2014年

Windows X64は、.exeの起動時に突然のアンダーフローの設定を渡しますが、Windows 32ビットおよびLinuxは渡しません。Linuxでは、gcc -ffast-mathは突然のアンダーフローを設定するはずです(ただし、Windowsではそうは思いません)。インテルのコンパイラーはmain()で初期化することになっているので、これらのOSの違いは通過しませんが、私は噛まれて、プログラムで明示的に設定する必要があります。Sandy Bridgeで始まるIntel CPUは、加算/減算で発生する(ただし、除算/乗算ではない)非正規を効率的に処理するため、段階的なアンダーフローを使用する場合があります。
tim18

1
Microsoft / fp:fast(デフォルトではありません)は、gcc -ffast-mathまたはICL(デフォルト)/ fp:fastに固有の積極的な処理を行いません。ICL / fp:sourceに似ています。したがって、これらのコンパイラを比較する場合は、/ fp:(および場合によってはアンダーフローモード)を明示的に設定する必要があります。
tim18

18

gccでは、これでFTZとDAZを有効にできます。

#include <xmmintrin.h>

#define FTZ 1
#define DAZ 1   

void enableFtzDaz()
{
    int mxcsr = _mm_getcsr ();

    if (FTZ) {
            mxcsr |= (1<<15) | (1<<11);
    }

    if (DAZ) {
            mxcsr |= (1<<6);
    }

    _mm_setcsr (mxcsr);
}

gccスイッチも使用します:-msse -mfpmath = sse

(Carl Hetherington [1]に対応するクレジット)

[1] http://carlh.net/plugins/denormals.php


また、よりポータブルな丸め方(linux.die.net/man/3/fesetround)についてfesetround()fenv.h(C99で定義されています)を参照してください(ただし、これはすべてのFP演算に影響し、非正規化だけではありません
German Garcia

FTZに1 << 15および1 << 11が必要ですか?私は他で引用された1 << 15しか見たことがない...
イチジク

@fig:1 << 11はアンダーフローマスク用です。詳細はこちら:softpixel.com/~cwright/programming/simd/sse.php
ドイツのガルシア、

@GermanGarciaこれはOPの質問には答えません。質問は、「なぜこのコードが10倍高速に実行されるのか...」でした。この回避策を提供する前に、回答するか、コメントで提供してください。

9

ダン・ニーリーのコメントは答えに拡張されるべきです:

0.0f非正規化またはスローダウンを引き起こすのはゼロ定数ではなく、ループの各反復でゼロに近づく値です。それらがゼロに近づくにつれて、表現するためにより高い精度が必要になり、非正規化されます。これらはy[i]値です。(x[i]/z[i]すべてので1.0未満であるため、ゼロに近づきますi。)

コードの低速バージョンと高速バージョンの決定的な違いは、ステートメントy[i] = y[i] + 0.1f;です。ループの各反復でこの行が実行されるとすぐに、floatの余分な精度が失われ、その精度を表すために必要な非正規化は不要になります。その後、浮動小数点演算y[i]は非正規化されないため、高速のままです。

追加すると、余分な精度が失われるのはなぜ0.1fですか?浮動小数点数は有効桁数が非常に多いためです。少なくともこの例の浮動小数点形式では0.00001 = 1e-5、有効桁数が3桁の十分なストレージが0.00001 + 0.1 = 0.1あるとします。これは、最下位ビットをに格納するスペースがないため0.10001です。

要するに、y[i]=y[i]+0.1f; y[i]=y[i]-0.1f;それはあなたがそう思っているかもしれないノーオペレーションではありません。

神秘主義者もこれを言った:フロートの内容は、アセンブリコードだけでなく重要です。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.