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

pbpqsu0x  于 2023-03-20  发布在  其他
关注(0)|答案(7)|浏览(139)

为什么这段代码

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。我尚未使用其他编译器进行测试。

evrscar2

evrscar21#

**欢迎来到denormalized floating-point的世界!**它们可能会严重破坏性能!!!

反规范(或次规范)数是一种从浮点表示中获取一些非常接近零的额外值的黑客。反规范浮点上的操作可能比规范浮点上的操作慢***几十到几百倍。这是因为许多处理器不能直接处理它们,必须使用微代码捕获和解析它们。
如果在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的情况下编译代码)。
这意味着我们不使用这些奇怪的低精度几乎为零的值,而是舍入到零。

时序:酷睿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被转换/存储到两个循环之外的寄存器中,因此对性能没有影响。

hrirmatl

hrirmatl2#

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

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

cvtsi2ssq的速度确实慢了10倍。
显然,float版本使用的是从内存加载的XMM寄存器,而int版本使用cvtsi2ssq指令将int的真实的值0转换为float,这需要花费大量时间,将-O3传递给gcc也无济于事(gcc版本4.2.1)。
(使用double代替float并不重要,只是它会将cvtsi2ssq更改为cvtsi2sdq。)

更新

一些额外的测试表明它不一定是cvtsi2ssq指令。(使用int ai=0;float a=ai;和使用a而不是0),速度差异仍然存在,所以@Mysticial是对的,非规格化的浮点数造成了差异。这可以通过测试00.1f之间的值来看出。上述代码中的转折点大约在0.00000000000000000000000000000001处,此时循环突然花费了10倍的时间。

更新〈〈1

这个有趣现象的一个小形象化:

  • 第1列:浮点数,每次迭代除以2
  • 第2列:该浮点数的二进制表示
  • 第3列:将该浮动1 e7次求和所需的时间

可以清楚地看到,当开始反规格化时,指数(最后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的等价讨论可以在堆栈溢出问题 * Objective-C中的非规格化浮点?* 中找到。

yquaqz18

yquaqz183#

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

  • 在某些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上,英特尔编译器有默认禁用非规格化的选项。More details here
  • 编译器开关。-ffast-math-msse-mfpmath=sse将禁用非规格化运算并使其他一些操作更快,但不幸的是,它们也会执行许多其他近似运算,这可能会破坏您的代码。请仔细测试!Visual Studio编译器的快速数学等价物是/fp:fast,但我无法确认这是否也禁用了非规格化运算。1
ltqd579y

ltqd579y4#

丹·尼利的评论应该扩展为一个答案:
不是零常量0.0f被反规格化或导致速度减慢,而是每次循环迭代时接近零的值。随着它们越来越接近零,它们需要更高的精度来表示,因此变得反规格化。这些是y[i]值。(它们接近零是因为对于所有ix[i]/z[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;并不是您可能认为的无操作。
神秘主义者也这样说:浮点数的内容很重要,而不仅仅是汇编代码。
编辑:为了更好地说明这一点,即使机器操作码相同,也不是每个浮点操作都需要相同的时间来运行。对于某些操作数/输入,相同的指令需要更多的时间来运行。对于非规格化数尤其如此。

8e2ybdfx

8e2ybdfx5#

在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](http://carlh.net/plugins/denormals.php)

daupos2t

daupos2t6#

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

0.0f: 0.218s
0.1f: 0.127s

所以还是慢了,但不是10倍。

c8ib6hqw

c8ib6hqw7#

CPU在很长一段时间内对非规格化的数字只会慢一点。我的Zen2 CPU对非规格化的输入和非规格化的输出进行计算需要五个时钟周期,对规格化的数字需要四个时钟周期。
这是一个用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

如果能在评论中看到一些结果就好了。

相关问题