浮子不一致的乘法性能



在测试.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,发布构建,X86上的代码优化,没有附加的调试器。

正如其他人所提到的,当涉及亚正态浮点值时,各种处理器不支持正常速度计算。这是设计缺陷(如果行为会损害您的应用程序,或者是麻烦的话)或功能(如果您喜欢较便宜的处理器或替代使用硅的替代用途,而硅是通过不使用大门来进行此工作的)。

>

了解为什么在.5:

的过渡很有启发性

假设您乘以 p 。最终,该值变得很小,以至于结果是一定的亚正态值(低于2 -126 在32位IEEE二进制浮点数中)。然后乘法变得慢。当您继续乘以时,值继续减少,并且达到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快速衡量的代码?也许起始值或迭代次数已更改?

有一些方法可以解决此问题。一个是,硬件具有指定的模式设置,可以将使用或获得的任何亚正态值更改为零,称为" denormals为零"或"齐平至零"模式。我不使用.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);
    }

最新更新