为什么将 0.1f 更改为 0 会使性能降低 10 倍?

Why does changing 0.1f to 0 slow down performance by 10x?

提问人:GlassFish 提问时间:2/16/2012 最后编辑:Jonas SteinGlassFish 更新时间:3/14/2023 访问量:162549

问:

为什么这一段代码,

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 进行编译时。 优化级别为 enabled。 我没有使用其他编译器进行测试。-02sse2

C++ 性能 Visual-Studio-2010 编译 浮点

评论

12赞 James Kanze 2/17/2012
您是如何衡量差异的?编译时使用了哪些选项?
169赞 Michael Dorgan 2/17/2012
在这种情况下,为什么编译器不直接删除 +/- 0?!?
129赞 millimoose 2/17/2012
@Zyx2000 编译器并没有那么愚蠢。在 LINQPad 中反汇编一个微不足道的例子表明,无论你使用 、 、 ,甚至在需要 a 的上下文中,它都会吐出相同的代码。00f0d(int)0double
18赞 Otto Allmendinger 2/17/2012
什么是优化级别?
17赞 Vorac 5/10/2013
为什么编译器不删除 +/-0?

答:

433赞 mvds 2/17/2012 #1

对生成的程序集使用和应用差异只会产生以下差异:gcc

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

一个确实慢了 10 倍。cvtsi2ssq

显然,该版本使用从内存加载的 XMM 寄存器,而该版本将实际值 0 转换为使用指令,这需要花费大量时间。传递给 gcc 无济于事。(GCC 版本 4.2.1。floatintintfloatcvtsi2ssq-O3

(使用 instead of 无关紧要,只是它会将 .doublefloatcvtsi2ssqcvtsi2sdq

更新

一些额外的测试表明,它不一定是指令。一旦消除(使用 a 和 using 代替 ),速度差异仍然存在。因此,@Mysticial正确的话,非规范化浮点数会有所不同。这可以通过测试 和 之间的值来了解。上述代码中的转折点大约是 ,此时循环突然花费了 10 倍的时间。cvtsi2ssqint ai=0;float a=ai;a000.1f0.00000000000000000000000000000001

更新<<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 中的非规范化浮点?中找到。

评论

28赞 leftaroundabout 2/17/2012
-Os 不修复它,但确实如此。(我一直在使用它,IMO 无论如何,它导致精度问题的极端情况不应该出现在正确设计的程序中。-ffast-math
0赞 Jed 3/12/2012
gcc-4.6 在任何正优化级别都没有转换。
3赞 Peter Cordes 1/16/2019
@leftaroundabout:编译一个可执行文件(不是库),其中包含一些额外的启动代码,这些代码在MXCSR中设置FTZ(刷新为零)和DAZ(非正常为零),因此CPU永远不必对非正常化进行缓慢的微码辅助。-ffast-math
1722赞 Mysticial 2/17/2012 #2

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

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

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

然后,带有的版本不再慢 10 倍,实际上变得更快。(这要求在启用 SSE 的情况下编译代码。0

这意味着,我们不再使用这些奇怪的低精度几乎为零的值,而是四舍五入到零。

计时: 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

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

评论

112赞 s73v3r 2/17/2012
我仍然觉得编译器默认没有完全优化“+ 0”有点奇怪。如果他把“+ 0.0f”放进去,会发生这种情况吗?
55赞 Mysticial 2/17/2012
@s73v3r 这是一个很好的问题。现在我看了一下装配,甚至没有优化出来。如果我不得不猜测,如果碰巧是信号或其他东西,可能会有副作用......不过我可能是错的。+ 0.0f+ 0.0fy[i]NaN
16赞 Russell Borogove 2/17/2012
在许多情况下,双打仍然会遇到同样的问题,只是数值大小不同。刷新到零对于音频应用程序(以及其他您可以承受在这里和那里丢失 1e-38 的应用程序)来说很好,但我相信不适用于 x87。如果没有 FTZ,音频应用的通常解决方法是注入一个非常低振幅(听不见)的直流或方波信号,以使数字抖动远离异常。
18赞 Dan Is Fiddling By Firelight 2/17/2012
@Isaac,因为当 y[i] 明显小于 0.1 时,相加会导致精度损失,因为数字中的最高有效数字会变得更高。
203赞 Eric Postpischil 7/7/2012
@s73v3r:+0.f 无法优化出来,因为浮点数为负 0,将 +0.f 加到 -.0f 的结果是 +0.f。所以添加 0.f 不是身份操作,无法优化出来。
20赞 German Garcia 10/2/2012 #3

在 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

评论

0赞 German Garcia 10/2/2012
另请参阅 from(为 C99 定义)了解另一种更可移植的舍入方式 (linux.die.net/man/3/fesetround)(但这会影响所有 FP 操作,而不仅仅是次正态fesetround()fenv.h)
0赞 fig 2/26/2014
您确定需要 1<<15 和 1<<11 用于自由贸易区吗?我只看到其他地方引用了 1<<15......
0赞 German Garcia 2/27/2014
@fig:1<<11 用于下溢蒙版。更多信息在这里: softpixel.com/~cwright/programming/simd/sse.php
0赞 7/25/2014
@GermanGarcia这并不能回答 OP 问题;问题是“为什么这一段代码的运行速度比......”快 10 倍 - 您应该在提供此解决方法之前尝试回答这个问题,或者在评论中提供此方法。
40赞 fig 2/26/2014 #4

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

  • 在某些 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 上禁用非规范化的选项。更多细节在这里

  • 编译器开关。,或者会禁用异常化并使其他一些事情更快,但不幸的是,还会执行许多其他可能破坏代码的近似值。仔细测试!相当于 Visual Studio 编译器的快速数学,但我无法确认这是否也禁用了非正态。1-ffast-math-msse-mfpmath=sse/fp:fast

评论

4赞 Ben Voigt 6/22/2014
这听起来像是一个不同但相关的问题的体面答案(如何防止数值计算产生非正态结果?不过,它并没有回答这个问题。
1赞 tim18 6/12/2016
Windows X64 在启动 .exe 时会传递突然下溢设置,而 Windows 32 位和 Linux 则不会。在 linux 上,gcc -ffast-math 应该设置突然的下溢(但我认为在 Windows 上不是)。英特尔编译器应该在 main() 中初始化,这样这些操作系统差异就不会传递,但我被咬了,需要在程序中显式设置它。从 Sandy Bridge 开始的 Intel CPU 应该可以有效地处理加/减(但不是除/乘)中出现的次正常现象,因此可以使用渐进下溢。
2赞 tim18 6/12/2016
Microsoft /fp:fast(不是默认值)不会执行 gcc -ffast-math 或 ICL(默认)/fp:fast 中固有的任何激进操作。它更像是 ICL /fp:source。因此,如果要比较这些编译器,则必须显式设置 /fp:(在某些情况下,必须设置下溢模式)。
31赞 remcycles 8/1/2018 #5

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

不是非规范化或导致速度变慢的零常量,而是每次循环迭代接近零的值。当它们越来越接近零时,它们需要更高的精度来表示,并且它们变得非规范化。这些是值。(它们接近零,因为所有 .0.0fy[i]x[i]/z[i]i

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

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

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

Mystical 也说过:浮点数的内容很重要,而不仅仅是汇编代码。

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

评论

0赞 shadowtalker 11/9/2022
我认为这个答案是对实际提出的问题的最佳答案,而不是通常有趣且写得很好。
0赞 user6562673 9/18/2022 #6

CPU 在很长一段时间内只对非正态数字慢一点。我的 Zen2 CPU 需要 5 个时钟周期才能进行非正态输入和非正态输出的计算,以及 4 个具有规范化数字的时钟周期。

这是一个用 Visual C++ 编写的小型基准测试,用于显示非正态数字的轻微性能降级效果:

#include <iostream>
#include <cstdint>
#include <chrono>

using namespace std;
using namespace chrono;

uint64_t denScale( uint64_t rounds, bool den );

int main()
{
    auto bench = []( bool den ) -> double
    {
        constexpr uint64_t ROUNDS = 25'000'000;
        auto start = high_resolution_clock::now();
        int64_t nScale = denScale( ROUNDS, den );
        return (double)duration_cast<nanoseconds>( high_resolution_clock::now() - start ).count() / nScale;
    };
    double
        tDen = bench( true ),
        tNorm = bench( false ),
        rel = tDen / tNorm - 1;
    cout << tDen << endl;
    cout << tNorm << endl;
    cout << trunc( 100 * 10 * rel + 0.5 ) / 10 << "%" << endl;
}

这是 MASM 装配部件。

PUBLIC ?denScale@@YA_K_K_N@Z

CONST SEGMENT
DEN DQ 00008000000000000h
ONE DQ 03FF0000000000000h
P5  DQ 03fe0000000000000h
CONST ENDS

_TEXT SEGMENT
?denScale@@YA_K_K_N@Z PROC
    xor     rax, rax
    test    rcx, rcx
    jz      byeBye
    mov     r8, ONE
    mov     r9, DEN
    test    dl, dl
    cmovnz  r8, r9
    movq    xmm1, P5
    mov     rax, rcx
loopThis:
    movq    xmm0, r8
REPT 52
    mulsd   xmm0, xmm1
ENDM
    sub     rcx, 1
    jae     loopThis
    mov     rdx, 52
    mul     rdx
byeBye:
    ret
?denScale@@YA_K_K_N@Z ENDP
_TEXT ENDS
END

很高兴在评论中看到一些结果。

1赞 Huy Le 3/14/2023 #7

2023 年更新,在 Ryzen 3990x、gcc 10.2 上,编译选项,2 版本之间的区别是-O3 -mavx2 -march=native

0.0f: 0.218s
0.1f: 0.127s

所以它仍然很慢,但不是慢 10 倍。

评论

0赞 GlassFish 5/26/2023
-O3 可能会针对非规范化浮点进行优化