为什么这段代码
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
。我尚未使用其他编译器进行测试。
7条答案
按热度按时间evrscar21#
**欢迎来到denormalized floating-point的世界!**它们可能会严重破坏性能!!!
反规范(或次规范)数是一种从浮点表示中获取一些非常接近零的额外值的黑客。反规范浮点上的操作可能比规范浮点上的操作慢***几十到几百倍。这是因为许多处理器不能直接处理它们,必须使用微代码捕获和解析它们。
如果在10,000次迭代后打印出这些数字,您将看到它们收敛到不同的值,具体取决于使用的是
0
还是0.1
。下面是在x64上编译的测试代码:
输出:
请注意,在第二轮中,数字非常接近于零。
反规格化的数字通常很少出现,因此大多数处理器不会尝试有效地处理它们。
为了证明这与反规格化的数字有很大关系,如果我们在代码的开头添加以下代码,将反规格化的数字清除为零:
然后,使用
0
的版本不再慢10倍,实际上变得更快了(这需要在启用SSE的情况下编译代码)。这意味着我们不使用这些奇怪的低精度几乎为零的值,而是舍入到零。
时序:酷睿i7 920在3.5 GHz时:
最后,这真的与它是整数还是浮点无关,
0
或0.1f
被转换/存储到两个循环之外的寄存器中,因此对性能没有影响。hrirmatl2#
使用
gcc
并对生成的程序集应用diff只会产生以下差异: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倍的时间。更新〈〈1
这个有趣现象的一个小形象化:
可以清楚地看到,当开始反规格化时,指数(最后9位)变为最小值。此时,简单加法的速度会慢20倍。
关于ARM的等价讨论可以在堆栈溢出问题 * Objective-C中的非规格化浮点?* 中找到。
yquaqz183#
这是由于非规格化浮点数的使用。如何消除它和性能损失?在互联网上搜索了杀死非规格化数的方法,似乎还没有“最好的”方法来做到这一点。我发现以下三种方法在不同的环境中可能效果最好:
-ffast-math
、-msse
或-mfpmath=sse
将禁用非规格化运算并使其他一些操作更快,但不幸的是,它们也会执行许多其他近似运算,这可能会破坏您的代码。请仔细测试!Visual Studio编译器的快速数学等价物是/fp:fast
,但我无法确认这是否也禁用了非规格化运算。1ltqd579y4#
丹·尼利的评论应该扩展为一个答案:
不是零常量
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;
并不是您可能认为的无操作。神秘主义者也这样说:浮点数的内容很重要,而不仅仅是汇编代码。
编辑:为了更好地说明这一点,即使机器操作码相同,也不是每个浮点操作都需要相同的时间来运行。对于某些操作数/输入,相同的指令需要更多的时间来运行。对于非规格化数尤其如此。
8e2ybdfx5#
在gcc中,您可以使用以下命令启用FTZ和DAZ:
也使用gcc交换机:-msse -mfpmath=sse
(对应学分卡尔赫瑟林顿[1])
[1][http://carlh.net/plugins/denormals.php](http://carlh.net/plugins/denormals.php)
daupos2t6#
2023年更新,在锐龙3990x上,gcc 10.2,编译选项
-O3 -mavx2 -march=native
,2个版本之间的区别是所以还是慢了,但不是10倍。
c8ib6hqw7#
CPU在很长一段时间内对非规格化的数字只会慢一点。我的Zen2 CPU对非规格化的输入和非规格化的输出进行计算需要五个时钟周期,对规格化的数字需要四个时钟周期。
这是一个用Visual C++编写的小型基准测试,用于显示非规格化数对性能的轻微影响:
这是MASM组件零件。
如果能在评论中看到一些结果就好了。