为什么这段代码,

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。我还没有用其他编译器测试过。


当前回答

丹·尼利的评论应该扩展为一个答案:

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

该代码的慢版本和快版本之间的关键区别是语句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;这不是你可能认为的不可行。

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

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

其他回答

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

在某些GCC环境中可能不起作用://需要#include<fenv.h>fesetenv(FE_DFL_DISABLE_SSE_DENORMS_ENV);在某些Visual Studio环境中可能无法工作:1//需要#include<xmmintrin.h>_mm_setcsr(_mm_getcsr()|(1<<15)|(1<<6));//同时执行FTZ和DAZ位。您也可以只使用十六进制值0x8040来执行这两个操作。//您可能还需要使用下溢掩码(1<<11)似乎可以在GCC和Visual Studio中使用://需要#include<xmmintrin.h>//需要#include<pmmintrin.h>_MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON);_MM_SET_demormals_ZERO_MODE(_MM_demormals_szero_ON);英特尔编译器具有在现代英特尔CPU上默认禁用非规格化的选项。此处显示更多详细信息编译器开关-ffast-math、-msse或-fmpmath=sse将禁用非正规化,并使一些其他操作更快,但不幸的是,也会执行许多其他近似操作,这可能会破坏代码。仔细测试!Visual Studio编译器的快速数学等价物是/fp:fast,但我无法确认这是否也禁用了非规格化。1

在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-fmpmath=sse

(对应于Carl Hetherington[1])

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

丹·尼利的评论应该扩展为一个答案:

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

该代码的慢版本和快版本之间的关键区别是语句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;这不是你可能认为的不可行。

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

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

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

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

如果在10000次迭代后打印出这些数字,您将看到它们已经收敛到不同的值,这取决于使用的是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

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

在很长一段时间内,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

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