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 进行计算。
以下 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 进行计算。