.NET 零开销抽象指南中,哪些能揭示最佳实践?

摘要:背景 2008 年前后的 Midori 项目试图构建一个以 .NET 为用户态基础的操作系统,在这个项目中有很多让 CLR 以及 C# 的类型系统向着适合系统编程的方向改进的探索,虽然项目最终没有面世,但是积累了很多的成果。近些年由于 .N
背景 2008 年前后的 Midori 项目试图构建一个以 .NET 为用户态基础的操作系统,在这个项目中有很多让 CLR 以及 C# 的类型系统向着适合系统编程的方向改进的探索,虽然项目最终没有面世,但是积累了很多的成果。近些年由于 .NET 团队在高性能和零开销设施上的需要,从 2017 年开始,这些成果逐渐被加入 CLR 和 C# 中,从而能够让 .NET 团队将原先大量的 C++ 基础库函数用 C# 重写,不仅能减少互操作的开销,还允许 JIT 进行 inline 等优化。 与常识可能不同,将原先 C++ 的函数重写成 C# 之后,带来的结果反而是大幅提升了运行效率。例如 Visual Studio 2019 的 16.5 版本将原先 C++ 实现的查找与替换功能用 C# 重写之后,更是带来了超过 10 倍的性能提升,在十万多个文件中利用正则表达式查找字符串从原来的 4 分多钟减少只需要 20 多秒。 目前已经到了 .NET 7 和 C# 11,我们已经能找到大量的相关设施,不过我们仍处在改进进程的中途。 本文则利用目前为止已有的设施,讲讲如何在 .NET 中进行零开销的抽象。 基础设施 首先我们来通过以下的不完全介绍来熟悉一下部分基础设施。 ref、out、in 和 ref readonly 谈到 ref 和 out,相信大多数人都不会陌生,毕竟这是从 C# 1 开始就存在的东西。这其实就是内存安全的指针,允许我们在内存安全的前提之下,享受到指针的功能: void Foo(ref int x) { x++; } int x = 3; ref int y = ref x; y = 4; Console.WriteLine(x); // 4 Foo(ref y); Console.WriteLine(x); // 5 而 out 则多用于传递函数的结果,非常类似 C/C++ 以及 COM 中返回调用是否成功,而实际数据则通过参数里的指针传出的方法: bool TryGetValue(out int x) { if (...) { x = default; return false; } x = 42; return true; } if (TryGetValue(out int x)) { Console.WriteLine(x); } in 则是在 C# 7 才引入的,相对于 ref 而言,in 提供了只读引用的功能。通过 in 传入的参数会通过引用方式进行只读传递,类似 C++ 中的 const T*。 为了提升 in 的易用性,C# 为其加入了隐式引用传递的功能,即调用时不需要在调用处写一个 in,编译器会自动为你创建局部变量并传递对该变量的引用: void Foo(in Mat3x3 mat) { mat.X13 = 4.2f; // 错误,因为只读引用不能修改 } // 编译后会自动创建一个局部变量保存这个 new 出来的 Mat3x3 // 然后调用函数时会传递对该局部变量的引用 Foo(new() { }); struct Mat3x3 { public float X11, X12, X13, X21, X22, X23, X31, X32, X33; } 当然,我们也可以像 ref 那样使用 in,明确指出我们引用的是什么东西: Mat3x3 x = ...; Foo(in x); struct 默认的参数传递行为是传递值的拷贝,当传递的对象较大时(一般指多于 4 个字段的对象),就会发生比较大的拷贝开销,此时只需要利用只读引用的方法传递参数即可避免,提升程序的性能。
阅读全文