为什么将0.1f更改为0会使性能降低10倍
Why does changing 0.1f to 0 slow down performance by 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 SP1进行编译时。优化级别为启用了sse2
的-02
。我还没有用其他编译器进行过测试。
欢迎来到非规范化浮点的世界他们会对表演造成严重破坏!!!
非正规(或亚正规)数字是一种从浮点表示中获得一些非常接近零的额外值的方法。非规范化浮点上的操作可能比规范化浮点慢几十到几百倍。这是因为许多处理器无法直接处理它们,必须使用微码捕获并解析它们。
如果在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
被转换/存储到两个循环之外的寄存器中。所以这对性能没有影响。
使用gcc
并对生成的程序集应用diff只会产生以下差异:
73c68,69
< movss LCPI1_0(%rip), %xmm1
---
> movabsq $0, %rcx
> cvtsi2ssq %rcx, %xmm1
81d76
< 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
),速度差仍然存在。所以@Mysticial是对的,非规范化的浮点值起了作用。这可以通过测试0
和0.1f
之间的值来看出。上述代码中的转折点大约在0.00000000000000000000000000000001
,此时循环的时间突然增加了10倍。
更新<lt;1
这个有趣现象的一个小可视化:
- 第1列:浮点值,每次迭代除以2
- 第2列:此浮点的二进制表示
- 第3列:将此浮点值相加1e7次所花费的时间
当反规范化开始时,你可以清楚地看到指数(最后9位)变为其最低值。在这一点上,简单加法会慢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
关于ARM的等效讨论可以在Stack中找到;溢出问题Objective-C中的非规范化浮点。
这是由于使用了非规范化浮点。如何摆脱它和性能惩罚?在互联网上搜索了杀死非正规数字的方法后,似乎还没有"最好"的方法。我发现这三种方法可能在不同的环境中效果最好:
-
在某些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);
-
英特尔编译器具有在现代英特尔CPU上默认禁用非标准化的选项。更多详情点击此处
-
编译器开关。
-ffast-math
、-msse
或-mfpmath=sse
将禁用去规范化,并使其他一些事情更快,但不幸的是,它们还执行了许多其他可能会破坏代码的近似。仔细测试!Visual Studio编译器的快速数学等价物是/fp:fast
,但我还无法确认这是否也禁用了非标准化。1
不是零常数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也说过:浮动的内容很重要,而不仅仅是汇编代码。
EDIT:更详细地说,即使机器操作码相同,也不是每个浮点运算都需要相同的运行时间。对于某些操作数/输入,运行同一指令将花费更多时间。这对于非正规化数字尤其如此。
在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
2023年更新,在Ryzen 3990x上,gcc 10.2,编译选项-O3 -mavx2 -march=native
,两个版本之间的区别是
0.0f: 0.218s
0.1f: 0.127s
所以它仍然更慢,但不会慢10倍。
CPU在很长一段时间内对非标准化数字只会慢一点。我的Zen2CPU需要五个时钟周期来进行具有非标准化输入和非标准化输出的计算,以及四个具有标准化数字的时钟周期。
这是一个用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
如果能在评论中看到一些结果,那就太好了。
- 将一个双倍值乘以10会发生什么
- 数组中的不同浮点值会影响性能 10 倍 - 为什么?
- 每个编译器的C++性能,比C#慢200倍
- C++,自使用boost和std::chrono的纪元以来的时间?为什么 Boost 版本慢 10 倍?
- 如何在 C++ 中将 2 中的数字倍数倍数在 2 中倍数 10 倍?
- 内存移动的性能与memcpy相比是两倍?
- C++随机生成器在g++中比MSVC快10倍
- localtime() 比 Linux 上的 gmtime() 性能问题多 24 倍
- 特征3.3.0对3.2.10的性能回归
- 打开'write'文件所需的时间是打开文件'read'的 10-50 倍
- 找到以 10 为底的双倍的指数和尾数
- 相同的功能?使用 GMP(C++) 时运行速度慢约 10 倍
- 为什么为"Hello World"程序生成的代码对C++比为 C 生成的代码大 10 倍?
- 为什么将0.1f更改为0会使性能降低10倍
- Python套接字,下载几乎是原始文件大小的10倍,上传是0字节
- 普通c++代码比内联汇编器快10倍.为什么
- 将文件读入结构体时大小增加10倍
- 当我所做的只是将循环代码移动到函数中时,代码运行速度会慢10倍
- OpenCL示例程序在CPU上的执行速度是在GPU上的10倍
- 当在gcc中使用O2时,这个向量如何被优化10倍