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

2021-01-30 21:46:02

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&lt; 9000000; j ++){for(int i = 0; i&lt; 16; i ++){y [i] * = x [i]; y [i] / = z [i]; y [i] = y [i] + 0.1f; //&lt;-y [i] = y [i]-0.1f; //&lt;-}}

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&lt; 9000000; j ++){for(int i = 0; i&lt; 16; i ++){y [i] * = x [i]; y [i] / = z [i]; y [i] = y [i] + 0; //&lt;-y [i] = y [i]-0; //&lt;-}}

使用Visual Studio 2010 SP1进行编译时。启用sse2的优化级别为-02。我尚未与其他编译器进行过测试。

您如何衡量差异?编译时使用了哪些选项? 詹姆斯·坎泽

在这种情况下,为什么编译器不只是降低+/- 0? 迈克尔·多根(Michael Dorgan)

@ Zyx2000编译器不在那个愚蠢的地方。在LINQPad中反汇编一个简单的示例表明,无论在需要双精度的情况下使用0、0f,0d还是(int)0,它都会吐出相同的代码。 – Millimoose

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

如果在10,000次迭代后打印出数字,您会发现它们已经收敛为不同的值,具体取决于使用0还是0.1。

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};浮点y [16]; for(int i = 0; i&lt; 16; i ++){y [i] = x [i]; } for(int j = 0; j&lt; 9000000; j ++){for(int i = 0; i&lt; 16; i ++){y [i] * = x [i]; y [i] / = z [i];#ifdef浮动y [i] = y [i] + 0.1f; y [i] = y [i] -0.1f; #else y [i] = y [i] +0; y [i] = y [i] -0; #endif如果(j&gt; 10000)cout&lt; y [i]&lt;&lt; &#34; &#34 ;; }如果(j&gt; 10000)cout&lt;&lt;恩德尔} double end = omp_get_wtime(); cout&lt;&lt;结束-开始&lt;&lt;恩德尔系统(&#34;暂停&#34;);返回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

非规范化的数字通常很少见,因此大多数处理器不会尝试有效地处理它们。

为了证明这与非规范化数字有关,如果我们通过将非规范化数字添加到代码的开头来将其归零,则将其归零:

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

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

//不要将反常态刷新为零0.1f:0.5640670:26.7669 //将反常态刷新为零0.1f:0.5871170:0.341406

最后,这确实与整数或浮点数无关。 0或0.1f转换/存储到两个循环外部的寄存器中。因此,这对性能没有影响。

我仍然觉得&#34; + 0&#34;有点奇怪默认情况下,编译器未完全对其进行优化。如果他放了&#34; + 0.0f&#34;会发生这种情况吗? – s73v3r

@ s73v3r这是一个很好的问题。现在,我看看程序集,甚至+ 0.0f都没有得到优化。如果我不得不猜测的话,如果y [i]恰好是信号NaN或其他信号,那么+ 0.0f可能会有副作用。 – Mysticial

在许多情况下,双打仍然会遇到相同的问题,只是数值大小不同。齐零归零对于音频应用程序(以及在其他地方您可能会损失1e-38的其他应用程序)很好,但是我相信这不适用于x87。如果不使用FTZ,则通常用于音频应用的解决方案是注入一个非常低的幅度(无法听到)的DC或方波信号,以使抖动次数远离异常。 –罗素·波罗戈夫(Russell Borogove)

@Isaac,因为当y [i]显着小于0.1时,由于数字中的最高有效位数变高,因此会导致精度损失。 –丹爱玩火光

@ s73v3r:无法优化+ 0.f,因为浮点的值为负0,并且将+ 0.f添加到-.0f的结果为+ 0.f。因此,添加0.f并不是身份操作,因此无法进行优化。 –埃里克·Postpischil

显然,浮点版本使用从内存加载的XMM寄存器,而int版本使用cvtsi2ssq指令将实际的int值0转换为float,这会花费很多时间。将-O3传递给gcc并没有帮助。 (gcc版本4.2.1)。

(使用double代替float没关系,只不过它将cvtsi2ssqq更改为cvtsi2sdq。)

一些额外的测试表明,它不一定是cvtsi2ssq指令。一旦消除(使用int ai = 0;浮点a = ai;并使用a而不是0),则速度差仍然存在。因此,@ Mysticial是正确的,非规范化的浮点数会有所作为。通过测试0到0.1f之间的值可以看出这一点。上面的代码中的转折点大约为0.00000000000000000000000000000000000001,这时循环突然花了10倍的时间。

您可以清楚地看到,在进行非规格化设置时,指数(最后9位)变为最低值。这时,简单加法会慢20倍。

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.000000000000000000000000000000000000000000195312509121:10111100001101110010000101000000 43 ms0.00000000000000000000000000000000000000000000000005656561:10111100001101110010000001000000 42 ms0.00000000 0000000000000000000000000000048828127280: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.000000000000000000000000000000000000000000000000000095366768:00011011100100001000000000000000 798 ms0.000000000000000000000000000000000000000000000000000683683:00110111001000010000000000000000000000 791 ms0.0000000000 00000000000000000000000000000023841692: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.000000000000000000000000000000000000000000000000046243:10000100000000000000000000000000 861 ms0.000000000000000000000000000000000000000000000000000000022421:00001000000000000000000000000000 855 ms0.000000000 000000000000000000000000000000000000000011210:00010000000000000000000000000000 887 ms0.000000000000000000000000000000000000000000000000000000005560:00100000000000000000000000000000000000 799 ms0.00000000000000000000000000000000000000000000000000000000000000000000000000300000001:100000000000000000000000000000000 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000

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

-Os不能修复它,但是-ffast-math可以修复。 (我一直在使用IMO,因为这总是会导致精度问题,这在任何情况下都不应在设计正确的程序中出现。) –关于

@leftaroundabout:使用-ffast-math编译可执行文件(不是库)会链接一些额外的启动代码,这些启动代码将MXCSR中的FTZ(刷新为零)和DAZ(反常为零)设置为零,因此CPU永远不需要慢速微代码辅助对于异常。 –彼得·科德斯(Peter Cordes)

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

//需要#include&lt; xmmintrin.h&gt; _mm_setcsr(_mm_getcsr()|(1&lt;&lt; 15)|(1&lt;&lt; 6)); //同时执行FTZ和DAZ位。您也可以只使用十六进制值0x8040来完成这两个操作。//您可能还想使用下溢掩码(1 <&lt; 11)

英特尔编译器具有在现代英特尔CPU上默认情况下禁用反常态的选项。在这里更多细节

编译器开关。 -ffast-math,-msse或-mfpmath = sse将禁用异常,并使其他一些事情变得更快,但不幸的是,它还会进行许多其他近似处理,可能会破坏您的代码。仔细测试!对于Visual Studio编译器来说,快速运算的等效项是/ fp:fast,但是我无法确认这是否也禁用了异常。 1个

这听起来像是对一个不同但相关的问题的正确回答(但是,如何防止数值计算产生不正常的结果?),它并不能回答这个问题。 – Ben Voigt

Windows X64启动.exe时会通过突然下溢的设置,而Windows 32位和Linux不会。在linux上,gcc -ffast-math应该设置突然的下溢(但我认为在Windows上不是)。英特尔编译器应该在main()中进行初始化,以便不会传递这些操作系统差异,但是我被人咬了,需要在程序中进行显式设置。以Sandy Bridge开头的Intel CPU应该能够有效地处理加/减(但不能除/乘)中产生的次正态,因此有必要使用渐进式下溢。 – tim18

Microsoft / fp:fast(不是默认值)不会执行gcc -ffast-math或ICL(默认值)/ fp:fast固有的任何攻击性行为。它更像是ICL / fp:source。因此,如果要比较这些编译器,则必须显式设置/ fp :(在某些情况下,还应设置为下溢模式)。 – tim18

#include&lt; xmmintrin.h&gt; #define FTZ 1#define DAZ 1 void enableFtzDaz(){int mxcsr = _mm_getcsr();如果(FTZ){mxcsr | =(1&lt;&lt; 15)| (1 << 11); }如果(DAZ){mxcsr | =(1&lt;&lt; 6); } _mm_setcsr(mxcsr);}

另请参阅fenv.h(为C99定义)中的fesetround(),以获得另一种更可移植的舍入方式(linux.die.net/man/3/fesetround)(但这会影响所有FP操作,而不仅仅是子范式) –德国加西亚

您确定FTZ需要1&lt;&lt; 15和1&lt;&lt; 11吗?我只看到其他地方引用了1≤15 ... - 图

@GermanGarcia这没有回答OP的问题;问题是为什么为什么这段代码比...运行速度快10倍? -您应该在提供此替代方法之前尝试回答该问题,或者在评论中提供它。 – user719662

归一化或导致减慢的不是零常数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中的最低有效位。 神秘主义者也这样说:浮点数的内容很重要,而不仅仅是汇编代码。 编辑:要对此进行更详细的说明,即使机器操作码相同,也不是每个浮点操作都需要花费相同的时间来运行。 对于某些操作数/输入,同一条指令将花费更多时间来运行。 对于非正规数尤其如此。 不是您要找的答案? 浏览其他标记的问题或提出自己的问题。