为什么将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 2012年

158
在这种情况下,为什么编译器不只是降低+/- 0?
Michael Dorgan

127
@ Zyx2000编译器不在那个愚蠢的地方。在拆卸显示LINQPad一个简单的例子,它吐出来的是相同的代码,您是否使用00f0d,甚至(int)0在上下文其中double需要。
millimoose 2012年

14
优化级别是多少?
奥托·阿曼丁格

Answers:


1615

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

非正规(或非正规)数字是一种破解,可以从浮点表示中获得非常接近于零的一些额外值。可以对非规范化浮点进行运算比在标准化浮点上的操作慢几十到数百倍。这是因为许多处理器无法直接处理它们,而必须使用微码来捕获和解析它们。

如果在10,000次迭代后打印出数字,您将看到它们已经收敛到不同的值,具体取决于是否 00.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转换/存储到两个环路的一个寄存器外。因此,这对性能没有影响。


100
我仍然觉得有点奇怪,因为默认情况下编译器没有完全优化“ + 0”。如果他放“ + 0.0f”会发生这种情况吗?
s73v3r 2012年

51
@ s73v3r这是一个很好的问题。现在,我查看装配,甚至没有+ 0.0f进行优化。如果我不得不猜测,+ 0.0f如果y[i]碰巧是一个信号NaN或诸如此类的东西,那可能会有副作用。但是我可能是错的。
Mysticial

14
在许多情况下,双打仍然会遇到相同的问题,只是数值大小不同。音频清零对音频应用程序(以及在其他地方可能会损失1e-38的应用程序)很好,但我认为不适用于x87。如果没有FTZ,通常用于音频应用的解决方案是注入一个非常低的振幅(听不到)的DC或方波信号,以使抖动次数远离异常。
罗素·博罗戈夫

16
@Isaac,因为当y [i]显着小于0.1时,由于数字中的最高有效位数变高,因此会导致精度损失。
Dan在火光中摆弄

167
@ s73v3r:无法优化+ 0.f,因为浮点的值为负0,并且将+ 0.f添加到-.0f的结果为+ 0.f。因此,添加0.f并不是标识操作,因此无法进行优化。
埃里克·波斯特皮希尔

415

使用gccdiff并将其应用于生成的程序集只会产生以下差异:

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

cvtsi2ssq一个是慢10倍确实如此。

显然,该float版本使用从内存中加载的XMM寄存器,而该int版本则将实int数值0 转换为float使用cvtsi2ssq指令,这会花费大量时间。传递-O3给gcc并没有帮助。(gcc版本4.2.1)。

(使用double代替float没关系,只是将更cvtsi2ssq改为cvtsi2sdq。)

更新资料

一些额外的测试表明,它不一定是cvtsi2ssq指令。一旦消除(使用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

可以在Stack Overflow问题Objective-C中的非规范化浮点中找到关于ARM的等效讨论


27
-Os不能解决,但是-ffast-math可以解决。(我一直在使用IMO,因为它总是会导致精度问题,在任何情况下都不会在设计正确的程序中出现。)
大约

对于gcc-4.6,在任何积极的优化级别上都没有转换。
2012年

@leftaroundabout:通过-ffast-math链接编译可执行文件(不是库),并在MXCSR中设置一些额外的启动代码,这些代码将FTZ(刷新为零)和DAZ(反常为零)设置为零,因此CPU无需为慢速进行慢速微码辅助。
彼得·科德斯

34

这是由于使用了非规范化的浮点数。如何摆脱它和性能损失?搜寻Internet来消除异常数字的方法之后,似乎尚无“最佳”方法。我发现这三种方法可能在不同的环境中效果最好:

  • 在某些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);
  • 英特尔编译器具有在现代英特尔CPU上默认情况下禁用反常态的选项。在这里更多细节

  • 编译器开关。-ffast-math-msse或者-mfpmath=sse将禁用异常,并使其他一些事情变得更快,但不幸的是,还进行了许多其他近似处理,可能会破坏您的代码。仔细测试!与Visual Studio编译器的快速运算相当,/fp:fast但我无法确认这是否也禁用了异常。1个


1
这听起来像是对一个不同但相关的问题的正确答案(我如何防止数值计算产生不正常的结果?)尽管如此,它并未回答。
Ben Voigt 2014年

Windows X64启动.exe时会通过突然下溢的设置,而Windows 32位和Linux不会。在linux上,gcc -ffast-math应该设置突然的下溢(但我认为不是在Windows上)。英特尔编译器应该在main()中进行初始化,以便不会传递这些操作系统差异,但是我被人咬了,需要在程序中对其进行显式设置。以Sandy Bridge开头的Intel CPU应该能够有效地处理加/减(但不能除/乘)中产生的次正态,因此存在使用渐进下溢的情况。
tim18 2016年

1
Microsoft / fp:fast(不是默认值)不会执行gcc -ffast-math或ICL(默认)/ fp:fast中固有的任何攻击性功能。它更像是ICL / fp:source。因此,如果要比较这些编译器,则必须显式设置/ fp:(在某些情况下,还应设置为下溢模式)。
2016年

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

(相当于学分卡尔·赫瑟灵顿[1])

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


还看到fesetround()fenv.h用于(为C99定义的)另一个,四舍五入(更便携的方式linux.die.net/man/3/fesetround)(但是这会影响到所有的FP操作,不只是次归
德国加西亚

您确定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;。在循环的每次迭代中执行此行后,浮点数中的额外精度就会丢失,并且不再需要代表该精度的非规范化。之后,y[i]由于没有进行非规范化,因此浮点运算仍然保持快速状态。

为什么添加时会失去额外的精度0.1f?因为浮点数只有很多有效数字。假设您有足够的存储空间来存储三个有效数字,然后0.00001 = 1e-5是和0.00001 + 0.1 = 0.1,至少对于本示例的float格式而言,因为没有足够的空间来存储中的最低有效位0.10001

简而言之,y[i]=y[i]+0.1f; y[i]=y[i]-0.1f;这不是您可能想的那样。

神秘主义者也这样说:浮点数的内容很重要,而不仅仅是汇编代码。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.