1. ホーム
  2. c++

[解決済み] 0.1fを0にすると、なぜ10倍もパフォーマンスが落ちるのですか?

2022-03-16 02:08:37

質問

なぜ、このようなコードがあるのか。

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,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);

というバージョンでは 0 は、もはや10倍も遅くはなく、むしろ速くなるのです。(これには、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 は、両方のループの外側でレジスタに変換/格納されます。そのため、パフォーマンスには影響がありません。