C# 中的 in 参数和性能分析

in 修饰符也是从 C# 7.2 开始引入的,它与我们上一篇中讨论的 《C# 中的只读结构体(readonly struct)》 是紧密相关的。

in 修饰符

in 修饰符通过引用传递参数。 它让形参成为实参的别名,即对形参执行的任何操作都是对实参执行的。 它类似于 ref 或 out 关键字,不同之处在于 in 参数无法通过调用的方法进行修改。

ref 修饰符,指定参数由引用传递,可以由调用方法读取或写入。

out 修饰符,指定参数由引用传递,必须由调用方法写入。

in 修饰符,指定参数由引用传递,可以由调用方法读取,但不可以写入。

举个简单的例子:

struct Product { public int ProductId { get; set; } public string ProductName { get; set; } } public static void Modify(in Product product) { //product = new Product(); // 错误 CS8331 无法分配到 变量 'in Product',因为它是只读变量 //product.ProductName = "测试商品"; // 错误 CS8332 不能分配到 变量 'in Product' 的成员,因为它是只读变量 Console.WriteLine($"Id: {product.ProductId}, Name: {product.ProductName}"); // OK } 引入 in 参数的原因

我们知道,结构体实例的内存在栈(stack)上进行分配,所占用的内存随声明它的类型或方法一起回收,所以通常在内存分配上它是比引用类型占有优势的。

但是对于有些很大(比如有很多字段或属性)的结构体,将其作为方法参数,在紧凑的循环或关键代码路径中调用方法时,复制这些结构的成本就会很高。当所调用的方法不修改该参数的状态,使用新的修饰符 in 声明参数以指定此参数可以按引用安全传递,可以避免(可能产生的)高昂的复制成本,从而提高代码运行的性能。

in 参数对性能的提升

为了测试 in 修饰符对性能的提升,我定义了两个较大的结构体,一个是可变的结构体 NormalStruct,一个是只读的结构体 ReadOnlyStruct,都定义了 30 个属性,然后定义三个测试方法:

DoNormalLoop 方法,参数不加修饰符,传入一般结构体,这是以前比较常见的做法。

DoNormalLoopByIn 方法,参数加 in 修饰符,传入一般结构体。

DoReadOnlyLoopByIn 方法,参数加 in 修饰符,传入只读结构体。

代码如下所示:

public struct NormalStruct { public decimal Number1 { get; set; } public decimal Number2 { get; set; } //... public decimal Number30 { get; set; } } public readonly struct ReadOnlyStruct { // 自动属性上的 readonly 关键字是可以省略的,这里加上是为了便于理解 public readonly decimal Number1 { get; } public readonly decimal Number2 { get; } //... public readonly decimal Number30 { get; } } public class BenchmarkClass { const int loops = 50000000; NormalStruct normalInstance = new NormalStruct(); ReadOnlyStruct readOnlyInstance = new ReadOnlyStruct(); [Benchmark(Baseline = true)] public decimal DoNormalLoop() { decimal result = 0M; for (int i = 0; i < loops; i++) { result = Compute(normalInstance); } return result; } [Benchmark] public decimal DoNormalLoopByIn() { decimal result = 0M; for (int i = 0; i < loops; i++) { result = ComputeIn(in normalInstance); } return result; } [Benchmark] public decimal DoReadOnlyLoopByIn() { decimal result = 0M; for (int i = 0; i < loops; i++) { result = ComputeIn(in readOnlyInstance); } return result; } public decimal Compute(NormalStruct s) { //业务逻辑... return 0M; } public decimal ComputeIn(in NormalStruct s) { //业务逻辑... return 0M; } public decimal ComputeIn(in ReadOnlyStruct s) { //业务逻辑... return 0M; } }

在没有使用 in 参数的方法中,意味着每次调用传入的是变量的一个新副本; 而在使用 in 修饰符的方法中,每次不是传递变量的新副本,而是传递同一副本的只读引用。

使用 BenchmarkDotNet 工具测试三个方法的运行时间,结果如下:

Method Mean Error StdDev Median Ratio RatioSD
DoNormalLoop   1,536.3 ms   65.07 ms   191.86 ms   1,425.7 ms   1.00   0.00  
DoNormalLoopByIn   480.9 ms   27.05 ms   79.32 ms   446.3 ms   0.32   0.07  
DoReadOnlyLoopByIn   581.9 ms   35.71 ms   105.30 ms   594.1 ms   0.39   0.10  

从这个结果可以看出,如果使用 in 参数,不管是一般的结构体还是只读结构体,相对于不用 in 修饰符的参数,性能都有较大的提升。这个性能差异在不同的机器上运行可能会有所不同,但是毫无疑问,使用 in 参数会得到更好的性能。

在 Parallel.For 中使用

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wssyff.html