题 为什么将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进行编译时。 (我没有和其他编译器一起测试过。)


1360
2018-02-16 15:58


起源


0 是一个整数文字,因此有可能必须在运行时将其转换为float。 - Zyx 2000
你是如何衡量这种差异的?您编译时使用了哪些选项? - James Kanze
在这种情况下,为什么编译器不会丢弃+/- 0? - Michael Dorgan
@ Zyx2000编译器不是那么愚蠢的地方。在LINQPad中反汇编一个简单的例子表明,无论你使用它,它都会吐出相同的代码 0, 0f, 0d, 甚至 (int)0 在一个上下文中 double 需要。 - millimoose
什么是优化级别? - Otto Allmendinger


答案:


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

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

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

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


1470
2018-02-16 16:20



我仍然觉得有点奇怪的是默认情况下编译器没有完全优化“+ 0”。如果他放“+ 0.0f”会发生这种情况吗? - s73v3r
@ s73v3r这是一个非常好的问题。现在我看着集会,甚至没有 + 0.0f 得到优化。如果我不得不猜测,可能就是这样 + 0.0f 会有副作用,如果 y[i] 恰巧是一个信号 NaN 什么......我可能错了。 - Mysticial
在许多情况下,双打仍然会遇到同样的问题,只是在不同的数值范围内。对于音频应用程序(以及其他你可以承受在这里和那里丢失1e-38的其他应用程序)来说,同等到零是好的,但我认为不适用于x87。如果没有FTZ,音频应用的常见修复方法是将非常低幅度(不可听见的)DC或方波信号注入到远离非正规性的抖动数。 - Russell Borogove
@Isaac因为当y [i]明显小于0.1时,加上它会导致精度损失,因为数字中最重要的数字会变得更高。 - Dan Neely
@ s73v3r:+ 0.f无法优化,因为浮点数为负0,将+ 0.f加到-.0f的结果为+ 0.f.因此,添加0.f不是身份操作,无法进行优化。 - Eric Postpischil


运用 gcc 并将diff应用于生成的程序集只会产生这种差异:

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

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


399
2018-02-16 16:19



-O不解决它,但是 -ffast-math 确实。 (我一直使用它,IMO在它引起精确故障的角落情况下不应该在正确设计的程序中出现。) - leftaroundabout
使用gcc-4.6在任何正优化级别都没有转换。 - Jed


这是由于非规范化的浮点使用。如何摆脱它和性能损失?在互联网上寻找杀死非正常数字的方法之后,似乎还没有“最好”的方法来做到这一点。我发现这三种方法在不同的环境中效果最好:

  • 可能无法在某些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


29
2018-02-26 12:15



这听起来像是对一个不同但相关的问题的正确答案(如何防止数值计算产生非正规结果?)但它并没有回答这个问题。 - Ben Voigt
@BenVoigt IFTFY - vaxquis
Windows X64在启动.exe时会传递突然下溢的设置,而Windows 32位和Linux则不会。在linux上,gcc -ffast-math应该设置突然下溢(但我认为不是在Windows上)。英特尔编译器应该在main()中初始化,以便这些操作系统差异不会通过,但我已经被咬了,需要在程序中明确设置它。从Sandy Bridge开始的Intel CPU应该有效地处理加/减(但不是除/乘)中出现的次正规,因此存在使用逐渐下溢的情况。 - tim18
Microsoft / fp:fast(不是默认值)不会执行gcc -ffast-math或ICL(默认)/ fp:fast中固有的任何攻击性事物。它更像ICL / fp:source。因此,如果要比较这些编译器,则必须明确设置/ fp :(在某些情况下,还应设置下溢模式)。 - tim18


在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


19
2017-10-02 04:40



另见 fesetround() 从 fenv.h (为C99定义)另一种更便携的四舍五入方式(linux.die.net/man/3/fesetround) (但是这个 会影响所有FP操作,而不仅仅是低于正常值) - German Garcia
你确定FTZ需要1 << 15和1 << 11吗?我只看到其他地方引用的1 << 15 ... - fig
@fig:1 << 11代表Underflow Mask。更多信息: softpixel.com/~cwright/programming/simd/sse.php - German Garcia
@GermanGarcia这不回答OP的问题;问题是“为什么这段代码运行速度比......快10倍” - 您应该在提供此变通方法之前尝试回答这个问题,或者在评论中提供此代码。 - vaxquis


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

它不是零常数 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,至少对于这个示例浮点格式,因为它没有空间存储最低有效位 0.10001

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

神秘也说了这个:浮动内容很重要,而不仅仅是汇编代码。


0
2017-08-01 13:32