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

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,未附加调试器。

最佳答案

正如其他人提到的那样,当涉及到非正常浮点值时,各种处理器不支持正常速度计算。这可能是设计缺陷(如果这种行为会损害您的应用程序或造成其他麻烦)或某个功能(如果您希望使用更便宜的处理器或通过不使用门来完成此工作而启用的硅的替代使用)。

了解为什么在.5处发生过渡很有启发性:

假设您乘以p。最终,该值变得如此之小,以至于结果是某个低于标准的值(在32位IEEE二进制浮点数中为2-126以下)。然后乘法变慢。随着您继续相乘,该值将继续减小,并达到2-149,这是可以表示的最小正数。现在,当您乘以p时,确切的结果当然是2-149p,它在0到2-149之间,这是两个最接近的可表示值。机器必须将结果取整并返回这两个值之一。

哪一个?如果p小于½,则2-149p比2-149更接近于0,因此机器返回0。然后您不再使用非正规值,并且乘法很快。如果p大于½,则2-149p比2-149更接近2-149,而不是0,因此机器返回2-149,并且您继续使用次正规值,并且乘法运算仍然很慢。如果p恰好是½,则舍入规则说使用在其有效位(分数部分)的低位具有零的值,该值是零(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的值太小,不会引起听音频的人可以观察到的任何效果。产生音频时,它可能太小而不会引起硬件行为的任何变化。

关于c# - 浮点数的乘法性能不一致,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/13964606/

10-09 16:04