背景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); // 4Foo(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 个字段的对象),就会发生比较大的拷贝开销,此时只需要利用只读引用的方法传递参数即可避免,提升程序的性能 。
从 C# 7 开始 , 我们可以在方法中返回引用,例如:
ref int Foo(int[] array){return ref array[3];}
调用该函数时 , 如果通过 ref
方式调用,则会接收到返回的引用:
int[] array = new[] { 1, 2, 3, 4, 5 };ref int x = ref Foo(array);Console.WriteLine(x); // 4x = 5;Console.WriteLine(array[3]); // 5
否则表示接收值,与返回非引用没有区别:
int[] array = new[] { 1, 2, 3, 4, 5 };int x = Foo(array);Console.WriteLine(x); // 4x = 5;Console.WriteLine(array[3]); // 4
与 C/C++ 的指针不同的是,C# 中通过 ref
显式标记一个东西是否是引用 , 如果没有标记 ref
,则一定不会是引用 。
当然 , 配套而来的便是返回只读引用,确保返回的引用是不可修改的 。与 ref
一样,ref readonly
也是可以作为变量来使用的:
ref readonly int Foo(int[] array){return ref array[3];}int[] array = new[] { 1, 2, 3, 4, 5 };ref readonly int x = ref Foo(array);x = 5; // 错误ref readonly int y = ref array[1];y = 3; // 错误
ref struct
C# 7.2 引入了一种新的类型:ref struct
。这种类型由编译器和运行时同时确保绝对不会被装箱,因此这种类型的实例的生命周期非常明确,它只可能在栈内存中,而不可能出现在堆内存中:
Foo[] foos = new Foo[] { new(), new() }; // 错误ref struct Foo{public int X;public int Y;}
借助 ref struct
推荐阅读
- 二、.Net Core搭建Ocelot
- 创建.NET程序Dump的几种姿势
- C# 8.0 添加和增强的功能【基础篇】
- .NET性能系列文章二:Newtonsoft.Json vs. System.Text.Json
- 某 .NET RabbitMQ SDK 有采集行为,你怎么看?
- .net core Blazor+自定义日志提供器实现实时日志查看器
- 学习ASP.NET Core Blazor编程系列九——服务器端校验
- 快读《ASP.NET Core技术内幕与项目实战》WebApi3.1:WebApi最佳实践
- 重新整理 .net core 实践篇 ———— linux上排查问题 [外篇]
- .NET API 接口数据传输加密最佳实践