高效率的 C#:能否接近 Rust 性能

高效率的 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. forforeach:到底推荐哪个?

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);
}

行为非常直接:

  1. i = 0 开始
  2. 每次判断 i < items.Length
  3. items[i]
  4. 调用 Handle(item)
  5. 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()
  • Current
  • Dispose()
  • 接口调用
  • 枚举器对象
  • 结构体枚举器
  • 装箱

注意:不是所有 foreach 都有这些成本。数组、List<T>Span<T> 都有很多优化。

但从极致可控的角度看,for 的隐藏行为更少。


13. 数组中的 forforeach

假设:

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);
}

看起来简单,但背后大概是:

  1. 创建 Where 迭代器对象
  2. 保存 lambda
  3. 每次 MoveNext()
  4. 调用 predicate
  5. 判断是否符合条件
  6. 返回当前值

普通业务代码完全可以接受。

但如果这是每秒调用几十万次的热路径,就要小心。

所以更准确的说法是:

高性能代码要避免抽象枚举和热路径中的 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)
{
}

如果 itemsIEnumerable<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);
}

主要原因是:

  1. 没有枚举器抽象
  2. 没有隐藏的 MoveNext() / Current 调用
  3. 更容易访问索引
  4. 更容易修改原元素
  5. 更容易让 JIT 消除边界检查
  6. 更容易做手动优化
  7. 更容易和 Span<T> / SIMD / unsafe 配合
  8. 性能行为更容易预测
  9. 可以避免不小心遍历 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 好,而是在性能敏感场景中更可控、更容易优化。