C# 在计算无穷大时速度减慢 30 倍

C# slows down 30x when computing with infinities

以下 C# 程序计算平方根的 1000 万次巴比伦迭代。

using System;
using System.Diagnostics;

namespace Performance {

    public class Program {
        public static void MeasureTime(long n, Action f) {
            Stopwatch watch = new Stopwatch();
            watch.Start();
            for (long i = 0; i < n; ++i) f();
            watch.Stop();
            Console.WriteLine($"{(n / watch.ElapsedMilliseconds) / 1000} Mop/s, {watch.ElapsedMilliseconds} ms");
        }

        public static void TestSpeed(double a) {
            Console.WriteLine($"Parameter {a}");
            double x = a;
            long n = 10_000_000;
            MeasureTime(n, () => x = (a / x + x) / 2);
            Console.WriteLine($"{x}\n");
        }

        static void Main(string[] args) {
            TestSpeed(2);
            TestSpeed(Double.PositiveInfinity);
        }
    }

}

当我在我的计算机上 运行 处于发布模式时,我得到:

Parameter 2
99 Mop/s, 101 ms
1,41421356237309

Parameter ∞
3 Mop/s, 3214 ms
NaN

这里Mop/s代表每秒百万次操作。当参数为无穷大时,出于某种原因,代码速度会降低 30 倍以上。

这是为什么?

为了比较,下面是用 C++20 编写的相同程序:

#include <iostream>
#include <chrono>
#include <format>

namespace Performance {

    template <typename F>
    void MeasureTime(long long n, F f) {
        auto begin = std::chrono::steady_clock::now();
        for (long long i = 0; i < n; ++i) f();
        auto end = std::chrono::steady_clock::now();
        auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(end - begin).count();
        std::cout << std::format("{0} Mop/s, {1} ms", (n / ms) / 1000, ms) << std::endl;
    }

    void TestSpeed(double a) {
        std::cout << std::format("Parameter {0}", a) << std::endl;
        double x = a;
        long long n = 10'000'000;
        MeasureTime(n, [&]() { x = (a / x + x) / 2; });
        std::cout << std::format("{0}\n\n", x);
    }

}

using namespace Performance;

int main() {
    auto inf = std::numeric_limits<double>::infinity();
    TestSpeed(2);
    TestSpeed(inf);
    return 0;
}

当我运行这个程序处于发布模式时,我得到:

Parameter 2
181 Mop/s, 55 ms
1.414213562373095

Parameter inf
192 Mop/s, 52 ms
-nan(ind)

符合预期;即性能上没有差异。

这两个程序均内置于 Visual Studio 2022 版本 17.1.0 中。 C# 项目是一个 Net Framework 4.7.2 控制台应用程序。

通过取消选中 C# 项目选项中的 Prefer 32-bits 解决了该问题。

通过将 Visual Studio 中的 Enable Enhanced Instruction Set 选项更改为 No Enhanced Instructions (/arch:IA32)Streaming SIMD Extensions (/arch:SSE),我还能够在 C++ 端重现性能问题。这些选项仅在构建 32 位程序时可用。正如@shingo 在评论中所暗示的那样,在较旧的 32 位指令集中使用 NaN 进行计算时似乎存在性能问题。实际上,当参数 a 设置为无穷大时,给定代码仅使用 NaN 进行计算。