为什么将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编译时。优化级别为-02,启用了sse2。我没有用其他编译器测试过。

158686 次浏览

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

73c68,69<   movss   LCPI1_0(%rip), %xmm1--->   movabsq $0, %rcx>   cvtsi2ssq   %rcx, %xmm181d76<   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),速度差异仍然存在。所以@Mysticia是对的,非规范化的浮点数会产生差异。这可以通过测试00.1f之间的值来看出。上面代码中的转折点大约在0.00000000000000000000000000000001,当循环突然需要10倍的时间时。

更新<<1

这个有趣现象的一个小可视化:

  • 第1列:一个浮点数,每次迭代除以2
  • 第2列:此浮点数的二进制表示
  • 第3栏:将此浮点数相加1e7次所需的时间

您可以清楚地看到,当非正则化设置时,指数(最后9位)变为其最低值。

0.000000000000000000000000000000000100000004670110: 10111100001101110010000011100000 45 ms0.000000000000000000000000000000000050000002335055: 10111100001101110010000101100000 43 ms0.000000000000000000000000000000000025000001167528: 10111100001101110010000001100000 43 ms0.000000000000000000000000000000000012500000583764: 10111100001101110010000110100000 42 ms0.000000000000000000000000000000000006250000291882: 10111100001101110010000010100000 48 ms0.000000000000000000000000000000000003125000145941: 10111100001101110010000100100000 43 ms0.000000000000000000000000000000000001562500072970: 10111100001101110010000000100000 42 ms0.000000000000000000000000000000000000781250036485: 10111100001101110010000111000000 42 ms0.000000000000000000000000000000000000390625018243: 10111100001101110010000011000000 42 ms0.000000000000000000000000000000000000195312509121: 10111100001101110010000101000000 43 ms0.000000000000000000000000000000000000097656254561: 10111100001101110010000001000000 42 ms0.000000000000000000000000000000000000048828127280: 10111100001101110010000110000000 44 ms0.000000000000000000000000000000000000024414063640: 10111100001101110010000010000000 42 ms0.000000000000000000000000000000000000012207031820: 10111100001101110010000100000000 42 ms0.000000000000000000000000000000000000006103515209: 01111000011011100100001000000000 789 ms0.000000000000000000000000000000000000003051757605: 11110000110111001000010000000000 788 ms0.000000000000000000000000000000000000001525879503: 00010001101110010000100000000000 788 ms0.000000000000000000000000000000000000000762939751: 00100011011100100001000000000000 795 ms0.000000000000000000000000000000000000000381469876: 01000110111001000010000000000000 896 ms0.000000000000000000000000000000000000000190734938: 10001101110010000100000000000000 813 ms0.000000000000000000000000000000000000000095366768: 00011011100100001000000000000000 798 ms0.000000000000000000000000000000000000000047683384: 00110111001000010000000000000000 791 ms0.000000000000000000000000000000000000000023841692: 01101110010000100000000000000000 802 ms0.000000000000000000000000000000000000000011920846: 11011100100001000000000000000000 809 ms0.000000000000000000000000000000000000000005961124: 01111001000010000000000000000000 795 ms0.000000000000000000000000000000000000000002980562: 11110010000100000000000000000000 835 ms0.000000000000000000000000000000000000000001490982: 00010100001000000000000000000000 864 ms0.000000000000000000000000000000000000000000745491: 00101000010000000000000000000000 915 ms0.000000000000000000000000000000000000000000372745: 01010000100000000000000000000000 918 ms0.000000000000000000000000000000000000000000186373: 10100001000000000000000000000000 881 ms0.000000000000000000000000000000000000000000092486: 01000010000000000000000000000000 857 ms0.000000000000000000000000000000000000000000046243: 10000100000000000000000000000000 861 ms0.000000000000000000000000000000000000000000022421: 00001000000000000000000000000000 855 ms0.000000000000000000000000000000000000000000011210: 00010000000000000000000000000000 887 ms0.000000000000000000000000000000000000000000005605: 00100000000000000000000000000000 799 ms0.000000000000000000000000000000000000000000002803: 01000000000000000000000000000000 828 ms0.000000000000000000000000000000000000000000001401: 10000000000000000000000000000000 815 ms0.000000000000000000000000000000000000000000000000: 00000000000000000000000000000000 42 ms0.000000000000000000000000000000000000000000000000: 00000000000000000000000000000000 42 ms0.000000000000000000000000000000000000000000000000: 00000000000000000000000000000000 44 ms

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

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

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

如果您在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 FLOATINGy[i]=y[i]+0.1f;y[i]=y[i]-0.1f;#elsey[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 FLOATING1.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-0071.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 FLOATING6.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-0446.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.5640670   : 26.7669
//  Flush denormals to zero.0.1f: 0.5871170   : 0.341406

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

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

(对应的学分给Carl Hetherington[1])

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

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

  • 可能在某些GCC环境中不起作用:

    // Requires #include <fenv.h>fesetenv(FE_DFL_DISABLE_SSE_DENORMS_ENV);
  • Might not work in some Visual Studio environments: 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)
  • Appears to work in both GCC and 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);
  • The Intel compiler has options to disable denormals by default on modern Intel CPUs. More details here

  • Compiler switches. -ffast-math, -msse or -mfpmath=sse will disable denormals and make a few other things faster, but unfortunately also do lots of other approximations that might break your code. Test carefully! The equivalent of fast-math for the Visual Studio compiler is /fp:fast but I haven't been able to confirm whether this also disables denormals.1

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

不是零常量0.0f被非规范化或导致减速,而是循环每次迭代都接近零的值。随着它们越来越接近零,它们需要更高的精度来表示,它们变得非规范化。这些是y[i]值。(它们接近零是因为x[i]/z[i]对所有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;不是你可能认为的无操作。

奥秘也这么说:浮点数的内容很重要,而不仅仅是汇编代码。

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

长期以来,CPU对于非正态数字只慢一点。我的Zen2 CPU需要五个时钟周期来进行具有非正态输入和非正态输出的计算,以及四个时钟周期来进行归一化数字。

这是一个使用VisualC++编写的小基准测试,以显示正态数的轻微性能下降效果:

#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;};doubletDen = 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 SEGMENTDEN DQ 00008000000000000hONE DQ 03FF0000000000000hP5  DQ 03fe0000000000000hCONST ENDS
_TEXT SEGMENT?denScale@@YA_K_K_N@Z PROCxor     rax, raxtest    rcx, rcxjz      byeByemov     r8, ONEmov     r9, DENtest    dl, dlcmovnz  r8, r9movq    xmm1, P5mov     rax, rcxloopThis:movq    xmm0, r8REPT 52mulsd   xmm0, xmm1ENDMsub     rcx, 1jae     loopThismov     rdx, 52mul     rdxbyeBye:ret?denScale@@YA_K_K_N@Z ENDP_TEXT ENDSEND

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