Why does changing 0.1f to 0 slow down performance by 10x?
为什么这段代码,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 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倍以上(相同,除非另有说明)?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 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编译时。(我没有用其他编译器进行测试。)
欢迎来到非规范化浮点的世界!他们会破坏表演!!!!
非正态(或次正态)数是一种从浮点表示中得到一些非常接近零的额外值的方法。非规范化浮点上的操作比规范化浮点上的操作慢几十到数百倍。这是因为许多处理器不能直接处理它们,必须使用微码捕获和解析它们。
如果您在10000次迭代之后打印出这些数字,您将看到它们已经收敛到不同的值,这取决于使用的是
以下是在x64上编译的测试代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | 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; } |
输出:
1 2 3 4 5 6 7 | #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 |
请注意,在第二次运行中,数字非常接近于零。
非规范化的数字通常很少,因此大多数处理器不尝试有效地处理它们。
为了证明这与非规范化的数字有关,如果我们通过在代码开头添加此值将非规范化值刷新为零:
1 | _MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON); |
那么,带有
这意味着,我们不使用这些奇怪的低精度几乎为零的值,而是将其舍入为零。
计时:核心I7 [email protected] GHz:
1 2 3 4 5 6 7 | // Don't flush denormals to zero. 0.1f: 0.564067 0 : 26.7669 // Flush denormals to zero. 0.1f: 0.587117 0 : 0.341406 |
最后,这实际上与它是整数还是浮点无关。
使用
1 2 3 4 5 6 7 | 73c68,69 < movss LCPI1_0(%rip), %xmm1 --- > movabsq $0, %rcx > cvtsi2ssq %rcx, %xmm1 81d76 < subss %xmm1, %xmm0 |
显然,
(使用
更新
一些额外的测试表明它不一定是
更新<<1
这一有趣现象的一个小形象化:
- 第1列:一个浮点,每次迭代除以2
- 第2列:这个浮点的二进制表示法
- 第3列:将此浮点数相加所用的时间为1e7次
当非规范化设置时,您可以清楚地看到指数(最后9位)更改为其最低值。在这一点上,简单的添加会慢20倍。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | 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的等效讨论可以在堆栈溢出问题中找到,目标C中的非规范化浮点?.
这是由于非规范化的浮点使用。如何摆脱它和绩效惩罚?在互联网上搜寻杀死非正常数字的方法之后,似乎还没有"最好"的方法来做到这一点。我发现这三种方法在不同的环境中效果最好:
可能无法在某些GCC环境中工作:
1
2// Requires #include <fenv.h>
fesetenv(FE_DFL_DISABLE_SSE_DENORMS_ENV);可能无法在某些Visual Studio环境中工作:1
1
2
3
4// 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中都有效:
1
2
3
4// 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);在现代Intel CPU上,"英特尔编译器"有默认禁用非规范化的选项。此处提供更多详细信息
编译器开关。
-ffast-math 、-msse 或-mfpmath=sse 将禁用非规范化并使其他一些事情更快,但不幸的是,也会进行许多其他可能破坏代码的近似。仔细测试!与Visual Studio编译器的快速数学等价的是/fp:fast ,但我还无法确认这是否也会禁用非规范化。1
在GCC中,您可以通过以下方式启用FTZ和DAZ:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | #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
丹·尼利的评论应该扩大到一个答案:
它不是非正规化的零常数
代码的慢速版本和快速版本之间的关键区别是语句
为什么添加
简言之,你可能会认为,江户记(14)不是不允许的。
神秘主义者也这么说:浮动的内容很重要,而不仅仅是汇编代码。