C数组VS.NET数组,小数组反转胜,大数组反转赢?
摘要:前几天在知乎看到一篇文章:《将一个序列反序,在C++与C#下性能比较》(链接大家可以自行搜索)。作者对比了 C# 的“托管非托管”实现和 C++ 的 std::rev
前几天在知乎看到一篇文章:《将一个序列反序,在C++与C#下性能比较》(链接大家可以自行搜索)。作者对比了 C# 的“托管/非托管”实现和 C++ 的 std::reverse_copy,最后得出的结论是:在小数组(1000 个元素)下 C++ 远超 .NET,而在大数据量下 .NET 非托管优于托管。
文章的切入点挺有意思,但作为老 .NET 开发者,我看完代码后发现这个对比其实没有控制好变量:C# 版本测试的是原地反转(In-place reverse,只做指针/索引交换,不分配新内存),而 C++ 版本用的是 std::reverse_copy 到一个新 vector 中(包含了内存分配和数据拷贝)。
这俩的语义完全不对等,底层的成本结构也完全不一样。拿“纯计算”去和“内存分配+拷贝”比性能,得出的结论很容易误导人。
好奇之下,我决定自己动手做个控制变量的公平测试:双方都只测纯粹的原地反转,并使用专业工具(BenchmarkDotNet 和自写的 C++ 高精度基准)跑一下。
结果非常有意思:小数组下 C++ 确实碾压,但数据量一上来,.NET 的 Array.Reverse 确实能反杀!
下面是完整的复现过程、代码和数据分析。
为什么原文章的对比不够公平?
我们先快速回顾一下原文章里的代码逻辑。
C# 原地反转(Span Slice 写法):
static void Reverse<T>(Span<T> span) {
while(span.Length > 1) {
T firstElement = span[0];
T lastElement = span[^1];
span[0] = lastElement;
span[^1] = firstElement;
span = span[1..^1]; // Slicing in each iteration introduces noticeable overhead
}
}
C++ 非原地反转(分配+拷贝):
// C++11
std::vector<int> test1() {
std::vector<int> rev(NumSize); // New allocation!
std::reverse_copy(vec.cbegin(), vec.cend(), rev.begin());
return rev;
}
发现问题了吗?C++ 每次都在 new 内存。在小数组测试中,内存分配的开销成了主导;在大数组测试中,带宽和缓存的影响又掩盖了纯粹的反转逻辑。原作者得出的“C++ > .NET”,更多是在测“分配+拷贝”的耗时,而不是单纯的反转算法效率。
控制变量:双方纯原地反转对决
为了得到准确的结论,我重新制定了测试规则:
纯原地操作:全部使用 std::reverse / Array.Reverse 或手写循环,绝不分配新数组。
防状态污染:每轮 Benchmark 前恢复原始数据(连续递增的 int 数组)。
防死代码消除 (DCE):对反转后的结果进行消费(计算 checksum)。
测试规模:N=1,000(测小规模调用的固定开销),N=1,000,000(测大规模吞吐量)。
.NET 端测试代码(BenchmarkDotNet)
为了探究极限,我写了四种实现:原生 Array.Reverse、Span 切片、常规下标以及 Unsafe 指针。
