与浮点数不一致的乘法性能

在.NET中测试浮点数的性能时,我偶然发现了一个奇怪的情况:对于某些值,乘法似乎比正常慢。 以下是测试用例:

using System; using System.Diagnostics; namespace NumericPerfTestCSharp { class Program { static void Main() { Benchmark(() => float32Multiply(0.1f), "\nfloat32Multiply(0.1f)"); Benchmark(() => float32Multiply(0.9f), "\nfloat32Multiply(0.9f)"); Benchmark(() => float32Multiply(0.99f), "\nfloat32Multiply(0.99f)"); Benchmark(() => float32Multiply(0.999f), "\nfloat32Multiply(0.999f)"); Benchmark(() => float32Multiply(1f), "\nfloat32Multiply(1f)"); } static void float32Multiply(float param) { float n = 1000f; for (int i = 0; i < 1000000; ++i) { n = n * param; } // Write result to prevent the compiler from optimizing the entire method away Console.Write(n); } static void Benchmark(Action func, string message) { // warm-up call func(); var sw = Stopwatch.StartNew(); for (int i = 0; i < 5; ++i) { func(); } Console.WriteLine(message + " : {0} ms", sw.ElapsedMilliseconds); } } } 

结果:

 float32Multiply(0.1f) : 7 ms float32Multiply(0.9f) : 946 ms float32Multiply(0.99f) : 8 ms float32Multiply(0.999f) : 7 ms float32Multiply(1f) : 7 ms 

为什么param = 0.9f的结果如此不同?

测试参数:.NET 4.5,发布版本,代码优化ON,x86,未附加调试器。

正如其他人所提到的,当涉及低于正常的浮点值时,各种处理器不支持正常速度计算。 这可能是一个设计缺陷(如果行为损害了您的应用程序或者其他方面很麻烦)或一个function(如果您更喜欢更便宜的处理器或通过不使用门来启用此工作而使用的硅的替代使用)。

了解为什么在.5处有转变是很有启发性的:

假设你乘以p 。 最终,该值变得很小,结果是一些次正常值(在32位IEEE二进制浮点下低于2 -126 )。 然后乘法变慢。 当您继续相乘时,该值继续递减,并且达到2 -149 ,这是可以表示的最小正数。 现在,当你乘以p时 ,确切的结果当然是2 -149 p ,介于0和2 -149之间,这是两个最接近的可表示值。 机器必须对结果进行舍入并返回这两个值中的一个。

哪一个? 如果p小于½,则2 -149 p更接近于0而不是2 -149 ,因此机器返回0.然后您不再使用次正规值,并且乘法再次快速。 如果p大于½,则2 -149 p更接近2 -149而不是0,因此机器返回2 -149 ,并继续使用次正规值,并且乘法仍然很慢。 如果p恰好是1/2,则舍入规则表示使用在其有效数的低位(分数部分)中具有零的值,该值为零(2 -149在其低位中具有1)。

您报告.99f看起来很快。 这应该以缓慢的行为结束。 也许您发布的代码并不完全是您使用.99f测量快速性能的代码? 也许起始值或迭代次数发生了变化?

有办法解决这个问题。 一个是硬件具有模式设置,其指定将所使用或获得的任何次正常值改变为零,称为“非正常为零”或“齐射到零”模式。 我不使用.NET,也不能告诉你如何在.NET中设置这些模式。

另一种方法是每次添加一个微小的值,例如

 n = (n+e) * param; 

其中e至少为2 -126 / param 。 注意,2 -126 / param应该向上舍入计算,除非你能保证n足够大以致(n+e) * param不产生次正规值。 这也假设n不是负数。 这样做的结果是确保计算值总是大到足以处于正常范围内,绝不是低于正常范围。

以这种方式添加e当然会改变结果。 但是,如果您正在处理具有某种回声效果(或其他滤波器)的音频,则e的值太小而不会导致人们听到音频时观察到的任何效果。 在生成音频时,它可能太小而不会导致硬件行为的任何变化。

我怀疑这与非正规值(fp值小于~1e-38)以及与处理它们相关的成本有关。

如果您测试非正常值并将其删除,则恢复健全性。

  static void float32Multiply(float param) { float n = 1000f; int zeroCount=0; for (int i = 0; i < 1000000; ++i) { n = n * param; if(n<1e-38)n=0; } // Write result to prevent the compiler from optimizing the entire method away Console.Write(n); }