为什么将 0.1f 更改为 0 会使性能降低 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.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 编译时。优化级别为 -02 sse2 。我没有用其他编译器测试过。

原文由 GlassFish 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 633
2 个回答

欢迎来到 非规范化浮点 的世界! 他们可以对性能造成严重破坏!!!

非正规(或次正规)数是一种从浮点表示中获得一些非常接近零的额外值的技巧。非规范化浮点的运算可能比规范化浮点 慢数十到数百倍。这是因为许多处理器不能直接处理它们,必须使用微码捕获和解析它们。

如果在 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

请注意,在第二次运行中,数字非常接近于零。

非规范化数字通常很少见,因此大多数处理器不会尝试有效地处理它们。


为了证明这与非规范化数字有关,如果我们通过将其添加到代码的开头将 非规范化刷新为零

 _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

最后,这实际上与它是整数还是浮点无关。 00.1f 被转换/存储到两个循环之外的寄存器中。所以这对性能没有影响。

原文由 Mysticial 发布,翻译遵循 CC BY-SA 3.0 许可协议

Dan Neely 的评论 应该扩展为一个答案:

不是零常数 0.0f 是非规范化或导致减速的,而是每次循环迭代接近零的值。随着它们越来越接近于零,它们需要更高的精度来表示并且它们变得非规范化。这些是 y[i] 值。 (它们接近于零,因为 x[i]/z[i] 对于所有 i 都小于 1.0。)

代码的慢版本和快版本之间的关键区别在于语句 y[i] = y[i] + 0.1f; 。一旦在循环的每次迭代中执行此行,浮点中的额外精度就会丢失,并且不再需要表示该精度所需的非规范化。之后, y[i] 上的浮点运算仍然很快,因为它们没有被非规范化。

为什么添加 0.1f 时会丢失额外的精度?因为浮点数只有这么多有效数字。假设您有足够的存储空间存储三个有效数字,然后 0.00001 = 1e-50.00001 + 0.1 = 0.1 至少对于这个示例浮点格式,因为它没有空间存储最低有效位 0.10001

简而言之, y[i]=y[i]+0.1f; y[i]=y[i]-0.1f; 不是您可能认为的无操作。

Mystical 也这么说:浮动的内容很重要,而不仅仅是汇编代码。

编辑:为了更好地说明这一点,即使机器操作码相同,也不是每个浮点运算都需要相同的时间来运行。对于某些操作数/输入,相同的指令将需要更多时间来运行。对于非正规数尤其如此。

原文由 remcycles 发布,翻译遵循 CC BY-SA 4.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题