13

在 .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,没有附加调试器。

4

2 回答 2

6

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

理解为什么在 0.5 处有过渡是很有启发性的:

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

哪一个?如果p小于 ½,则 2 -149 p比 2 -149更接近 0 ,因此机器返回 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太小而不会导致人类收听音频时可以观察到的任何效果。在生成音频时,它可能太小而不会导致硬件行为发生任何变化。

于 2012-12-20T14:47:58.073 回答
2

我怀疑这与非正规值(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);
    }
于 2012-12-20T03:43:23.137 回答