performance - 为什么0.1f更改为10x会降低性能?

  显示原文与译文双语对照的内容

为什么这段代码


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进行编译时。 ( 我还没有测试其他编译器。)

时间:

欢迎来到世界的非规范化 floating-point ! 他们会严重破坏性能 !

Denormal ( 或者普通的) 数字是一种 hack,它在浮点表示中非常接近于零。 上的操作可以将非规范化 floating-point 慢数十至数百倍 比在对标准化 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

注意在第二个运行中数字非常接近零。

非规范化数字通常是罕见的,因此大多数处理器都不会尝试高效地处理它们。


来展示,这个与反向规格化数,如果我们一切都只因为你做刷新denormals为0 通过添加这里代码的到开始:


_MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON);

那么 0的版本就不再是 10 x 更慢了,实际上更快。 ( 这需要使用SSE启用代码。)

这意味着不使用这些奇怪的低精度almost-zero值,而是四舍五入为零。

计时包括磁芯 i7 920 @ 3.5 1GHz:


//Don't flush denormals to zero.
0.1f: 0.564067
0 : 26.7669

//Flush denormals to zero.
0.1f: 0.587117
0 : 0.341406

最后,这与它是否是整数或者floating-point无关。 0 或者 0.1f 被转换/存储到两个循环之外的寄存器中。 这对性能没有影响。

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


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

cvtsi2ssq的速度确实是 10倍。

显然,float 版本使用从内存加载的调节寄存器,而 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: 计算这里浮点 1e7次所用的时间

可以清楚地看到,当非规范化设置为。 在那个时候,简单的加法变成了 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

一个等效的手臂中可以找到堆栈溢出问题探讨 objective-c 中不正常的浮点? 。

在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

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

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

这是因为floating-point使用不正常。 如何去掉它和性能损失? 在互联网上搜索了杀死denormal数字的方法后,似乎没有"最佳"的方法。 我发现这三种方法在不同的环境中可以达到最佳效果:

  • 在某些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);
    
    
  • 在现代的英特尔在英特尔编译器已经选项来禁用由 default. denormals 更多细节这里

  • 编译器开关。-ffast-math-msse 或者 -mfpmath=sse 将禁用denormals并使其他一些东西更快,但不幸的是,它也会使你的代码更容易出错。 测试仔细了,相当于为 Visual Studio 编译器是 /fp:fast fast-math但是我查不到也要确认这里禁用 denormals 。 1 栏标题

...