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

Asi*_*sik 13 .net c# floating-point performance

在.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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

结果:

float32Multiply(0.1f) : 7 ms
float32Multiply(0.9f) : 946 ms
float32Multiply(0.99f) : 8 ms
float32Multiply(0.999f) : 7 ms
float32Multiply(1f) : 7 ms
Run Code Online (Sandbox Code Playgroud)

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

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

Eri*_*hil 6

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

理解为什么在.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恰好是½,则舍入规则表示使用在其有效数的低位(零部分)中具有零的值,该值为零(2 -149在其低位中具有1).

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

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

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

n = (n+e) * param;
Run Code Online (Sandbox Code Playgroud)

哪里e至少是2 -126/param.请注意,2 -126/param应该向上舍入计算,除非您可以保证n足够大而(n+e) * param不会产生次正规值.这也假设n不是消极的.这样做的结果是确保计算值总是大到足以处于正常范围内,从不低于正常范围.

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

  • @spender:嗯,有一种可能性是实现使用80位浮点来进行算术运算,因此它具有比32位浮点数大得多的指数范围.然后.99情况可能永远不会达到80位次正常范围.增加迭代次数可能会发现这种情况. (2认同)