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 指针。
阅读全文