高效率的 C#:能否接近 Rust 性能,以及为什么热路径常用 for
1. C#/.NET 能不能写出性能接近 Rust 的代码?
可以。C#/.NET 在某些场景下可以写出非常接近 Rust 性能的代码,尤其是 CPU 计算、内存布局可控、避免频繁分配的场景。
但要注意:
"接近 Rust"可以做到;"所有场景都等同 Rust"不现实。
原因是两者的默认模型不同。
Rust 默认没有 GC,内存生命周期由所有权系统管理,释放点更可预测。C# 默认运行在 .NET 托管运行时上,有 GC,普通写法也更容易产生堆分配。
不过,如果 C# 写法足够偏底层、偏系统编程风格,就可以非常快。
2. C# 想接近 Rust,需要减少 GC 压力
如果在 C# 中做到这些:
- 减少堆分配
- 减少 GC 压力
- 使用
struct - 使用
readonly struct - 使用
ref struct - 使用
Span<T>/ReadOnlySpan<T> - 使用
Memory<T>/ReadOnlyMemory<T> - 使用
ArrayPool<T> - 使用
ObjectPool<T> - 使用
stackalloc - 避免热路径中的 LINQ
- 避免装箱
- 避免闭包分配
- 避免频繁创建短生命周期对象
- 使用
ref/in/out - 必要时使用
unsafe - 使用 SIMD,例如
System.Numerics.Vector<T>或硬件 intrinsics - 必要时使用 NativeAOT
那么在很多场景里,C# 的性能可以达到 Rust 的 80%~100%,某些场景甚至可以接近持平。
但如果写的是普通面向对象风格 C#:
csharp
var result = list
.Where(x => x.Enabled)
.Select(x => new Foo(x.Value))
.ToList();
这种代码通常离 Rust 会比较远。
原因不是 C# 的 JIT 一定差,而是这类写法可能带来:
- 分配多
- GC 压力大
- 迭代器对象
- 委托/lambda 成本
- 闭包分配
- 数据布局不够紧凑
- 抽象层较多
- 间接访问较多
3. C# 高性能写法的核心:少分配
Rust 中你可能会写:
rust
let mut buffer = Vec::with_capacity(n);
对象生命周期比较明确,释放点也比较可控。
C# 普通写法容易变成:
csharp
var list = new List<Item>();
var item = new Item();
如果大量 new class,就会增加 GC 压力。
C# 可以改成更高性能的写法。
例如使用栈内存:
csharp
Span<byte> buffer = stackalloc byte[256];
这不会进入 GC 堆。
或者使用数组池:
csharp
var array = ArrayPool<byte>.Shared.Rent(size);
try
{
// use array
}
finally
{
ArrayPool<byte>.Shared.Return(array);
}
这类写法就更接近手动控制内存的风格。
4. 普通写法与高性能写法对比
普通写法:
csharp
public static int Sum(int[] values)
{
return values.Where(x => x > 0).Sum();
}
这很简洁,但可能有额外迭代器、委托、LINQ 调用成本。
高性能写法:
csharp
public static int Sum(ReadOnlySpan<int> values)
{
var sum = 0;
for (var i = 0; i < values.Length; i++)
{
var value = values[i];
if (value > 0)
{
sum += value;
}
}
return sum;
}
这类代码通常会被 JIT 优化得很好,性能可以非常接近 C/Rust 风格的循环。
5. C# 的性能优势
现代 .NET 的性能已经很强,尤其是 .NET 6、.NET 7、.NET 8、.NET 9 之后。
5.1 JIT 很强
.NET 的 RyuJIT 可以做很多优化:
- 方法内联
- 去虚调用
- 边界检查消除
- 循环优化
- SIMD 向量化
- Tiered Compilation
- PGO,Profile-Guided Optimization
很多情况下,JIT 能根据运行时信息做出非常好的优化。
5.2 Span<T> 很重要
Span<T> 是高性能 C# 的关键工具。
它允许你安全地操作连续内存,而且通常不会产生额外堆分配。
csharp
public static void Fill(Span<byte> buffer)
{
for (var i = 0; i < buffer.Length; i++)
{
buffer[i] = 1;
}
}
适合:
- 网络协议解析
- 文件解析
- 序列化
- 反序列化
- 图像处理
- 加密
- 压缩
- 游戏逻辑
5.3 可以使用栈内存
csharp
Span<byte> temp = stackalloc byte[256];
这不会进入 GC 堆。
5.4 可以使用 unsafe
如果需要进一步压榨性能,C# 也可以写指针代码:
csharp
unsafe
{
fixed (byte* ptr = buffer)
{
// pointer operations
}
}
这更接近 C/C++/Rust 的底层控制能力。
6. Rust 仍然更有优势的地方
6.1 Rust 默认无 GC
Rust 没有 GC,内存释放点更可预测。
C# 即使减少分配,整个运行时仍然有 GC。你可以降低 GC 影响,但不能像 Rust 那样默认完全无 GC。
6.2 Rust 的所有权模型更强
Rust 编译器会阻止很多内存错误和数据竞争。
C# 要做到类似效果,需要依赖:
- 代码规范
- analyzer
- code review
- runtime checks
- 谨慎使用
unsafe
6.3 Rust 的数据布局更直接
Rust 很自然地写出紧凑的数据结构:
rust
struct Point {
x: f32,
y: f32,
}
C# 也可以:
csharp
public struct Point
{
public float X;
public float Y;
}
但如果 C# 使用 class,就是引用对象,会多一层间接访问,也会增加 GC 压力。
6.4 启动时间和部署体积
Rust 通常编译成本地二进制,启动快、运行时依赖少。
C# 默认依赖 .NET runtime,不过可以通过以下方式改善:
- NativeAOT
- trimming
- single-file publish
- ReadyToRun
7. 哪些场景 C# 可以接近 Rust?
C# 在这些场景里可以写得非常快:
- Web API
- 网络服务
- JSON 序列化/反序列化
- 数据处理
- 图像处理
- 数值计算
- 游戏逻辑
- 高频但可控的内存操作
- 协议解析
- 文件 IO
- 数据库客户端
- 缓存服务
ASP.NET Core 本身性能就很强,很多 benchmark 中表现很好。
8. 哪些场景 Rust 更有优势?
Rust 更适合:
- 操作系统组件
- 嵌入式
- 无运行时环境
- 极低延迟系统
- 高频交易
- 音频实时处理
- 数据库内核
- 浏览器引擎
- 高性能 CLI 工具
- 内存安全要求极高的底层组件
- 需要完全避免 GC 抖动的场景
9. C# 高性能代码要避免什么?
9.1 避免频繁 new class
csharp
var obj = new MyClass();
大量对象分配会增加 GC 压力。
可以考虑:
csharp
public struct MyStruct
{
}
或者使用对象池。
9.2 避免热路径中的 LINQ
少在热路径中写:
csharp
items.Select(x => x.Value).Where(x => x > 0).ToArray();
可以改成:
csharp
for (var i = 0; i < items.Length; i++)
{
// manual loop
}
9.3 避免装箱
csharp
object x = 123;
这会把 int 装箱到堆上。
接口调用也可能导致装箱:
csharp
IComparable c = 123;
9.4 避免闭包分配
csharp
int threshold = 10;
list.Where(x => x > threshold);
这种写法可能产生闭包对象。
9.5 小心字符串操作
字符串是不可变对象:
csharp
s += "abc";
如果在循环里这样写,会产生很多临时字符串。
应该考虑:
csharp
var builder = new StringBuilder();
或者使用 Span<char> / ValueStringBuilder 类似思路。
9.6 小心 async 分配
async/await 很好,但高频路径中也可能产生状态机和任务分配。
可以考虑:
csharp
ValueTask<T>
而不是总是使用:
csharp
Task<T>
10. C# 高性能工具箱
| 技术 | 用途 |
|---|---|
struct |
值类型,减少堆分配 |
readonly struct |
避免不必要修改,表达不可变语义 |
ref struct |
栈上结构,例如 Span<T> |
Span<T> |
安全操作连续内存 |
ReadOnlySpan<T> |
只读连续内存视图 |
Memory<T> |
可跨 async 的内存块 |
ArrayPool<T> |
复用数组,减少 GC |
ObjectPool<T> |
复用对象 |
stackalloc |
栈上分配 |
ValueTask<T> |
减少异步分配 |
Unsafe |
底层内存操作 |
fixed |
固定托管对象地址 |
| NativeAOT | 编译成本地程序 |
| SIMD intrinsics | 向量化加速 |
11. C# 与 Rust 的现实判断
如果目标是:
写业务系统、服务端、高性能 API,同时减少 GC,提高吞吐和延迟稳定性。
那么 C# 非常合适。它可以接近 Rust 级别的性能,而且开发效率通常更高。
如果目标是:
写数据库内核、实时音频、嵌入式、系统组件、无 GC 环境。
Rust 更合适。
一句话:
C# 能写出接近 Rust 性能的代码,但你要用"系统编程风格的 C#",而不是普通业务 C# 风格。
也就是多使用:
csharp
Span<T>
struct
ArrayPool<T>
stackalloc
manual loop
zero allocation
SIMD
NativeAOT
Rust 的优势是:它默认就把你往低分配、可控内存、安全并发的方向推。
C# 的特点是:你可以写得很快,但需要自觉避开高分配写法。
12. for 和 foreach:到底推荐哪个?
12.1 简短结论
不是所有地方都应该机械地把 foreach 改成 for。
对于数组、Span<T>、List<T> 来说,现代 .NET 中的 foreach 通常已经很快。
但是在性能敏感代码里,更常推荐:
csharp
for (var i = 0; i < items.Length; i++)
{
var item = items[i];
Handle(item);
}
原因是:
for更透明、更可控、更少隐藏行为,更容易配合底层优化。
12.2 什么是性能敏感代码?
性能敏感代码,也叫热路径,指的是程序中被频繁执行、非常在意每一点开销的代码。
例如游戏每帧更新:
csharp
for (var frame = 0; frame < 60; frame++)
{
UpdateGameObjects();
}
网络包解析:
csharp
while (true)
{
ReadNetworkPacket();
ParsePacket();
}
图像处理:
csharp
for (var i = 0; i < pixels.Length; i++)
{
ProcessPixel(pixels[i]);
}
这些地方循环可能执行几百万、几千万次。
如果每次循环都多一点点成本,例如:
- 多一次函数调用
- 多一次接口调用
- 多一个枚举器对象
- 多一次边界检查
- 多一层抽象
- 多一次拷贝
在普通代码里可能看不出来,但在高频循环中就可能变明显。
12.3 为什么 for 更适合热路径?
这段代码:
csharp
for (var i = 0; i < items.Length; i++)
{
var item = items[i];
Handle(item);
}
行为非常直接:
- 从
i = 0开始 - 每次判断
i < items.Length - 取
items[i] - 调用
Handle(item) i++
没有太多隐藏抽象。
而 foreach:
csharp
foreach (var item in items)
{
Handle(item);
}
语法上更简洁,但背后可能变成不同的东西,取决于 items 的类型。
它可能大致等价于:
csharp
var enumerator = items.GetEnumerator();
try
{
while (enumerator.MoveNext())
{
var item = enumerator.Current;
Handle(item);
}
}
finally
{
enumerator.Dispose();
}
也就是说,foreach 本质上是枚举器模式,可能涉及:
GetEnumerator()MoveNext()CurrentDispose()- 接口调用
- 枚举器对象
- 结构体枚举器
- 装箱
注意:不是所有 foreach 都有这些成本。数组、List<T>、Span<T> 都有很多优化。
但从极致可控的角度看,for 的隐藏行为更少。
13. 数组中的 for 和 foreach
假设:
csharp
int[] items = [1, 2, 3, 4];
写:
csharp
foreach (var item in items)
{
Handle(item);
}
对于数组,C# 编译器通常会特殊处理,优化成类似索引循环的形式:
csharp
for (var i = 0; i < items.Length; i++)
{
var item = items[i];
Handle(item);
}
所以数组上的 foreach 通常不慢。
但在性能敏感代码中,for 仍然常用,因为它有几个优势。
13.1 优势一:可以自然访问索引
csharp
for (var i = 0; i < items.Length; i++)
{
var item = items[i];
Handle(i, item);
}
如果使用 foreach,就要自己维护索引:
csharp
var i = 0;
foreach (var item in items)
{
Handle(i, item);
i++;
}
这种写法不如 for 直接。
13.2 优势二:方便修改原数组
例如把所有元素乘以 2:
csharp
for (var i = 0; i < items.Length; i++)
{
items[i] *= 2;
}
foreach 不适合这样写:
csharp
foreach (var item in items)
{
item *= 2;
}
因为 item 通常只是当前元素的局部副本,不是数组位置本身。
13.3 优势三:JIT 更容易识别某些模式
JIT 看到:
csharp
for (var i = 0; i < items.Length; i++)
{
var item = items[i];
}
很容易判断:
i从 0 开始i每次递增i永远小于items.Length- 所以
items[i]一定不会越界
于是 JIT 有机会消除一部分边界检查。
边界检查类似:
csharp
if ((uint)i >= (uint)items.Length)
{
throw new IndexOutOfRangeException();
}
C# 是安全语言,数组访问理论上要检查越界。
但在简单清晰的 for 循环中,JIT 可能证明它不会越界,从而优化掉这部分检查。
14. Span<T> 为什么尤其适合用 for?
Span<T> 常用于高性能场景。
例如:
csharp
Span<byte> buffer = stackalloc byte[1024];
或者:
csharp
ReadOnlySpan<byte> data = packet;
Span<T> 常用于:
- 网络协议解析
- 文件解析
- JSON 解析
- 二进制数据处理
- 加密
- 压缩
- 图像处理
这些场景本身就性能敏感,所以很多代码会写成:
csharp
public static int CountZeros(ReadOnlySpan<byte> data)
{
var count = 0;
for (var i = 0; i < data.Length; i++)
{
if (data[i] == 0)
{
count++;
}
}
return count;
}
当然,foreach 也可以:
csharp
public static int CountZeros(ReadOnlySpan<byte> data)
{
var count = 0;
foreach (var value in data)
{
if (value == 0)
{
count++;
}
}
return count;
}
现代 .NET 中这通常也很快。
但 for 更方便做底层操作,比如一次读取多个字节:
csharp
for (var i = 0; i < data.Length - 4; i += 4)
{
var value =
data[i]
| data[i + 1] << 8
| data[i + 2] << 16
| data[i + 3] << 24;
Handle(value);
}
这种代码用 foreach 就不自然。
for 更适合:
- 切片
- 跳步访问
- 一次处理多个元素
- 手动展开循环
- SIMD 优化
- unsafe 指针操作
15. List<T> 为什么说热路径中用 for?
List<T> 的 foreach 通常不分配堆内存。
csharp
foreach (var item in list)
{
Handle(item);
}
List<T>.Enumerator 是一个 struct,所以通常没有 GC 分配。
这点很重要:
不是说
List<T>的foreach一定会产生大量垃圾。一般不会。
但在热路径里,for 仍然可能更直接。
foreach 大致类似:
csharp
var enumerator = list.GetEnumerator();
while (enumerator.MoveNext())
{
var item = enumerator.Current;
Handle(item);
}
而 for 是:
csharp
for (var i = 0; i < list.Count; i++)
{
var item = list[i];
Handle(item);
}
foreach 多了一层枚举器语义。
虽然开销很小,但在极端热路径中,for 的行为更透明。
另外,List<T> 的 foreach 会检测集合是否被修改。
例如:
csharp
foreach (var item in list)
{
list.Add(item); // 运行时会抛异常
}
它会抛出:
csharp
InvalidOperationException
因为枚举过程中修改了集合。
这说明 List<T>.Enumerator 内部会记录版本号,并在 MoveNext() 时检查列表有没有变化。
这保证了安全性,但也是额外逻辑。
16. 真正需要小心的是 IEnumerable<T>
最大的问题通常不是 foreach 关键字本身,而是:
csharp
IEnumerable<Item> items = GetItems();
foreach (var item in items)
{
Handle(item);
}
这里你看不出 items 背后到底是什么。
它可能是数组:
csharp
Item[] items;
也可能是列表:
csharp
List<Item> items;
也可能是 LINQ:
csharp
items.Where(x => x.Enabled).Select(x => x.Value)
也可能是生成器:
csharp
IEnumerable<int> GetNumbers()
{
yield return 1;
yield return 2;
}
如果是 LINQ 或 yield return,背后可能有:
- 状态机
- 迭代器对象
- 委托调用
- 闭包
- 间接调用
- 延迟执行
例如:
csharp
foreach (var item in items.Where(x => x.Enabled))
{
Handle(item);
}
看起来简单,但背后大概是:
- 创建
Where迭代器对象 - 保存 lambda
- 每次
MoveNext() - 调用 predicate
- 判断是否符合条件
- 返回当前值
普通业务代码完全可以接受。
但如果这是每秒调用几十万次的热路径,就要小心。
所以更准确的说法是:
高性能代码要避免抽象枚举和热路径中的 LINQ,而不是盲目禁止
foreach。
17. 几个对比例子
17.1 数组求和
普通写法:
csharp
public static int Sum(int[] items)
{
var sum = 0;
foreach (var item in items)
{
sum += item;
}
return sum;
}
这通常已经很好。
高性能写法:
csharp
public static int Sum(int[] items)
{
var sum = 0;
for (var i = 0; i < items.Length; i++)
{
sum += items[i];
}
return sum;
}
这更直接,也更适合继续做底层优化。
17.2 过滤元素
LINQ 写法:
csharp
public static int CountEnabled(IEnumerable<Item> items)
{
return items.Where(x => x.Enabled).Count();
}
可读性很好,但热路径中不推荐。
foreach 写法:
csharp
public static int CountEnabled(IEnumerable<Item> items)
{
var count = 0;
foreach (var item in items)
{
if (item.Enabled)
{
count++;
}
}
return count;
}
比 LINQ 少一些抽象,但仍依赖 IEnumerable<T>。
高性能写法:
csharp
public static int CountEnabled(ReadOnlySpan<Item> items)
{
var count = 0;
for (var i = 0; i < items.Length; i++)
{
if (items[i].Enabled)
{
count++;
}
}
return count;
}
这个性能更可控。
17.3 修改元素
假设:
csharp
int[] values = [1, 2, 3];
想所有值加 1,推荐:
csharp
for (var i = 0; i < values.Length; i++)
{
values[i]++;
}
不要写:
csharp
foreach (var value in values)
{
value++;
}
因为 value 是副本,不会修改原数组。
18. 为什么 for 更接近 Rust 风格?
Rust 高性能代码通常强调:
- 明确的数据结构
- 明确的内存访问
- 少抽象
- 少分配
- 顺序访问
- 编译器容易优化
C# 中的:
csharp
for (var i = 0; i < items.Length; i++)
{
var item = items[i];
Handle(item);
}
就是一种很明确的顺序访问。
CPU 喜欢这种访问方式,因为:
- 内存连续
- 缓存友好
- 分支少
- 预测容易
- 向量化机会更高
尤其当 items 是数组或 Span<T> 时,数据通常是连续的,CPU 读取连续内存非常快。
19. 那是不是以后都不用 foreach?
不是。
可以按下面规则判断。
19.1 普通业务代码:用 foreach
csharp
foreach (var user in users)
{
SendEmail(user);
}
清晰、安全、不容易写错。
19.2 需要索引:用 for
csharp
for (var i = 0; i < users.Count; i++)
{
Process(i, users[i]);
}
19.3 要修改数组或 Span 元素:用 for
csharp
for (var i = 0; i < values.Length; i++)
{
values[i] *= 2;
}
19.4 热路径:优先考虑 for
csharp
for (var i = 0; i < buffer.Length; i++)
{
checksum += buffer[i];
}
19.5 遍历 IEnumerable<T>:要小心
csharp
foreach (var item in items)
{
}
如果 items 是 IEnumerable<T>,要问:
它背后到底是什么?数组?List?LINQ?yield return?数据库查询?
如果是热路径,最好不要让参数是:
csharp
IEnumerable<T>
可以考虑改成:
csharp
T[]
或者:
csharp
ReadOnlySpan<T>
或者:
csharp
IReadOnlyList<T>
20. 最核心总结
性能敏感代码里常推荐:
csharp
for (var i = 0; i < items.Length; i++)
{
var item = items[i];
Handle(item);
}
主要原因是:
- 没有枚举器抽象
- 没有隐藏的
MoveNext()/Current调用 - 更容易访问索引
- 更容易修改原元素
- 更容易让 JIT 消除边界检查
- 更容易做手动优化
- 更容易和
Span<T>/ SIMD /unsafe配合 - 性能行为更容易预测
- 可以避免不小心遍历 LINQ /
IEnumerable<T>带来的额外成本
但也要记住:
对数组、
List<T>、Span<T>来说,foreach在现代 .NET 中通常已经很快。
所以不是:
foreach一定慢。
而是:
for更透明、更低级、更可控,更适合写高性能 C#。
21. 最终建议
普通业务代码:
csharp
foreach (var item in items)
{
Handle(item);
}
性能敏感代码:
csharp
for (var i = 0; i < items.Length; i++)
{
var item = items[i];
Handle(item);
}
热路径中尽量避免:
csharp
foreach (var item in items.Where(x => x.Enabled))
{
Handle(item);
}
可以改成:
csharp
for (var i = 0; i < items.Length; i++)
{
var item = items[i];
if (!item.Enabled)
{
continue;
}
Handle(item);
}
最终一句话:
C# 可以写出接近 Rust 的性能,但需要使用低分配、数据连续、少抽象、热路径手写循环的风格。
for不是永远比foreach好,而是在性能敏感场景中更可控、更容易优化。