C++ vs .NET 数组原地反转实测:小数组 C++ 碾压,大数组 .NET 反杀?
前几天在知乎看到一篇文章:《将一个序列反序,在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 指针。
using System;
using System.Linq;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Exporters;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Order;
using BenchmarkDotNet.Running;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
BenchmarkRunner.Run<ReverseBench>(new Config());
public sealed class Config : ManualConfig
{
public Config()
{
AddJob(Job.ShortRun
.WithWarmupCount(3)
.WithIterationCount(8));
AddColumnProvider(DefaultColumnProviders.Instance);
AddExporter(MarkdownExporter.GitHub);
WithOrderer(new DefaultOrderer(SummaryOrderPolicy.FastestToSlowest));
}
}
[MemoryDiagnoser]
[RankColumn]
public class ReverseBench
{
private int[] _original = Array.Empty<int>();
private int[] _work = Array.Empty<int>();
[Params(1_000, 1_000_000)]
public int N;
[GlobalSetup]
public void Setup()
{
_original = Enumerable.Range(0, N).ToArray();
_work = new int[N];
}
[IterationSetup]
public void Reset()
{
Array.Copy(_original, _work, N);
}
[Benchmark(Baseline = true)]
public void ArrayReverse() => Array.Reverse(_work);
[Benchmark]
public void ManagedSpanSliceReverse() => ManagedSpanSlice(_work);
[Benchmark]
public void ManagedIndexReverse() => ManagedIndex(_work);
[Benchmark]
public void UnsafeSpanReverse() => UnsafeSpan(_work);
private static void ManagedSpanSlice(Span<int> span)
{
while (span.Length > 1)
{
int first = span[0];
int last = span[^1];
span[0] = last;
span[^1] = first;
span = span[1..^1];
}
}
private static void ManagedIndex(Span<int> span)
{
int i = 0;
int j = span.Length - 1;
while (i < j)
{
int tmp = span[i];
span[i] = span[j];
span[j] = tmp;
i++;
j--;
}
}
private static void UnsafeSpan(Span<int> span)
{
if (span.Length <= 1) return;
ref int left = ref MemoryMarshal.GetReference(span);
ref int right = ref Unsafe.Add(ref left, span.Length - 1);
do
{
int a = left;
int b = right;
left = b;
right = a;
left = ref Unsafe.Add(ref left, 1);
right = ref Unsafe.Subtract(ref right, 1);
} while (Unsafe.IsAddressLessThan(ref left, ref right));
}
}
(环境配置: Ubuntu 24.04, AMD EPYC 7763, .NET 10.0.3 RyuJIT AVX2)
C++ 端测试代码
C++ 这边同样准备了三种实现:std::reverse 标准库、手写下标和手写指针。为了对标 BenchmarkDotNet,我自己写了个高精度计时器。
#include <algorithm>
#include <chrono>
#include <cstdint>
#include <functional>
#include <iomanip>
#include <iostream>
#include <numeric>
#include <string>
#include <utility>
#include <vector>
using Clock = std::chrono::steady_clock;
// Prevent compiler from optimizing away the results
static inline void do_not_optimize(const void* p) {
asm volatile("" : : "g"(p) : "memory");
}
static void reverse_std(std::vector<int>& v) {
std::reverse(v.begin(), v.end());
}
static void reverse_index(std::vector<int>& v) {
if (v.size() <= 1) return;
size_t i = 0;
size_t j = v.size() - 1;
while (i < j) {
int t = v[i];
v[i] = v[j];
v[j] = t;
++i;
--j;
}
}
static void reverse_pointer(std::vector<int>& v) {
if (v.size() <= 1) return;
int* left = v.data();
int* right = v.data() + v.size() - 1;
while (left < right) {
int t = *left;
*left = *right;
*right = t;
++left;
--right;
}
}
struct Result {
std::string name;
int n;
int iterations;
double mean_ns;
double ns_per_element;
};
static Result bench(const std::string& name, int n, int iterations, const std::function<void(std::vector<int>&)>& fn) {
std::vector<int> original(n);
std::iota(original.begin(), original.end(), 0);
std::vector<int> work(n);
// Warmup
for (int i = 0; i < 3; ++i) {
work = original;
fn(work);
do_not_optimize(work.data());
}
auto start = Clock::now();
std::uint64_t checksum = 0;
for (int i = 0; i < iterations; ++i) {
work = original;
fn(work);
checksum += static_cast<std::uint64_t>(work[n / 2]);
do_not_optimize(work.data());
}
auto end = Clock::now();
do_not_optimize(&checksum);
double total_ns = std::chrono::duration<double, std::nano>(end - start).count();
double mean_ns = total_ns / iterations;
return {name, n, iterations, mean_ns, mean_ns / n};
}
int main() {
std::vector<int> sizes = {1000, 1000000};
std::vector<std::pair<std::string, std::function<void(std::vector<int>&)>>> fns = {
{"std::reverse", reverse_std},
{"manual_index", reverse_index},
{"manual_pointer", reverse_pointer},
};
std::cout << "impl,n,iterations,mean_ns,ns_per_element\n";
for (int n : sizes) {
int iterations = n <= 1000 ? 200000 : 600;
for (auto& [name, fn] : fns) {
auto r = bench(name, n, iterations, fn);
std::cout << r.name << ',' << r.n << ',' << r.iterations << ','
<< std::fixed << std::setprecision(2) << r.mean_ns << ','
<< std::fixed << std::setprecision(6) << r.ns_per_element << "\n";
}
}
return 0;
}
(环境配置: g++ 13.3.0, -O3 -march=native -std=c++20)
核心对决:谁才是真正的性能怪兽?
直接来看两边跑出来的最快成绩对比:
| 数组规模 (N) | C++ 最快实现 | 耗时 (Mean) | .NET 最快实现 | 耗时 (Mean) | 谁赢了? |
|---|---|---|---|---|---|
| 1,000 | manual_pointer | 150.83 ns | Array.Reverse | 445.60 ns | C++ 快 2.95x |
| 1,000,000 | manual_pointer | 162,917 ns | Array.Reverse | 88,716 ns | .NET 快 1.84x |


C++ 原始结果(CSV)
| impl | n | iterations | mean_ns | ns_per_element |
|---|---|---|---|---|
| std::reverse | 1000 | 200000 | 152.29 | 0.152291 |
| manual_index | 1000 | 200000 | 394.25 | 0.394251 |
| manual_pointer | 1000 | 200000 | 150.83 | 0.150829 |
| std::reverse | 1000000 | 600 | 199966.21 | 0.199966 |
| manual_index | 1000000 | 600 | 426880.66 | 0.426881 |
| manual_pointer | 1000000 | 600 | 162917.17 | 0.162917 |
.NET BenchmarkDotNet 完整结果
测试环境如下:
BenchmarkDotNet v0.14.0, Ubuntu 24.04.4 LTS (Noble Numbat) (container)
AMD EPYC 7763, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.103
[Host] : .NET 10.0.3 (10.0.326.7603), X64 RyuJIT AVX2
ShortRun : .NET 10.0.3 (10.0.326.7603), X64 RyuJIT AVX2
Job=ShortRun InvocationCount=1 IterationCount=8
LaunchCount=1 UnrollFactor=1 WarmupCount=3
| Method | N | Mean | Error | StdDev | Ratio | RatioSD | Rank |
|---|---|---|---|---|---|---|---|
| ArrayReverse | 1000 | 445.6 ns | 52.67 ns | 27.55 ns | 1.00 | 0.08 | 1 |
| ManagedIndexReverse | 1000 | 3,035.8 ns | 53.50 ns | 27.98 ns | 6.83 | 0.40 | 2 |
| UnsafeSpanReverse | 1000 | 4,873.0 ns | 436.03 ns | 193.60 ns | 10.97 | 0.75 | 3 |
| ManagedSpanSliceReverse | 1000 | 9,961.2 ns | 120.77 ns | 43.07 ns | 22.43 | 1.30 | 4 |
| ArrayReverse | 1000000 | 88,716.0 ns | 761.39 ns | 398.22 ns | 1.00 | 0.01 | 1 |
| UnsafeSpanReverse | 1000000 | 349,287.4 ns | 22,874.65 ns | 11,963.88 ns | 3.94 | 0.13 | 2 |
| ManagedIndexReverse | 1000000 | 412,116.6 ns | 18,274.90 ns | 9,558.12 ns | 4.65 | 0.10 | 2 |
| ManagedSpanSliceReverse | 1000000 | 501,977.0 ns | 25,291.77 ns | 13,228.09 ns | 5.66 | 0.14 | 2 |
为什么会出现这种两级反转?
-
小数组场景(C++ 的主场):
在处理 1000 个元素时,C++ 的指针版开销极小。没有边界检查,极致紧凑的循环,编译器直接将其优化到了硬件指令的极限。而 .NET 虽然Array.Reverse很快,但在小数组下,托管环境的方法调用开销、类型检查等固定成本占比就凸显出来了,导致略逊一筹。 -
大数组场景(.NET 的反杀):
当数据量来到 100 万时,.NET 的Array.Reverse(int[])展现出了恐怖的吞吐量,直接拉开了近一倍的差距。为什么?因为 .NET 运行时的Array.Reverse针对基元类型(Primitive types)做了深度优化,底层大概率走的是专属的 JIT 路径或高度优化的 SIMD/向量化指令。
反观我们自己手写的 Unsafe 代码或者原生 C++ 循环,如果没有显式进行向量化优化,在大吞吐量面前反而打不过官方的基础库。 -
永远不要盲目自信手写算法:
测试数据证实了原知乎文章里的一个现象:用 Span Slice 的写法确实是最慢的(切片开销大)。但同时我们也发现,在 .NET 中,哪怕你用上了Unsafe指针操作,依旧跑不过原生的Array.Reverse。这告诉我们:永远优先相信标准库。那帮写 Runtime 的微软大佬,底层的骚操作远比我们手写的while循环要多得多。
结语
抛开场景谈性能就是耍流氓。通过控制好“原地反转”这个核心变量,我们看到了 C++ 在微操作上的极致低开销,也看到了当代 .NET 在大数据吞吐和标准库优化上的强悍实力。
如果你觉得这种深扒底层细节的硬核性能分析有意思,或者也在做 C# / .NET 的高性能开发,欢迎在评论区聊聊你的看法。
也欢迎大家加入我的 .NET骚操作 QQ群:495782587,一起探讨更多硬核技术玩法!

浙公网安备 33010602011771号