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

在上面简单的 for 循环中,我们看到 in 参数有助于性能的提升,那么在并行运算中呢?我们把上面的 for 循环改成使用 Parallel.For 来实现,代码如下:

[Benchmark(Baseline = true)] public decimal DoNormalLoop() { decimal result = 0M; Parallel.For(0, loops, i => Compute(normalInstance)); return result; } [Benchmark] public decimal DoNormalLoopByIn() { decimal result = 0M; Parallel.For(0, loops, i => ComputeIn(in normalInstance)); return result; } [Benchmark] public decimal DoReadOnlyLoopByIn() { decimal result = 0M; Parallel.For(0, loops, i => ComputeIn(in readOnlyInstance)); return result; }

事实上,道理是一样的,在没有使用 in 参数的方法中,每次调用传入的是变量的一个新副本; 在使用 in 修饰符的方法中,每次传递的是同一副本的只读引用。

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

Method Mean Error StdDev Ratio
DoNormalLoop   793.4 ms   13.02 ms   11.54 ms   1.00  
DoNormalLoopByIn   352.4 ms   6.99 ms   17.27 ms   0.42  
DoReadOnlyLoopByIn   341.1 ms   6.69 ms   10.02 ms   0.43  

同样表明,使用 in 参数会得到更好的性能。

使用 in 参数需要注意的地方

我们来看一个例子,定义一个一般的结构体,包含一个属性 Value 和 一个修改该属性的方法 UpdateValue。 然后在别的地方也定义一个方法 UpdateMyNormalStruct 来修改该结构体的属性 Value。
代码如下:

struct MyNormalStruct { public int Value { get; set; } public void UpdateValue(int value) { Value = value; } } class Program { static void UpdateMyNormalStruct(MyNormalStruct myStruct) { myStruct.UpdateValue(8); } static void Main(string[] args) { MyNormalStruct myStruct = new MyNormalStruct(); myStruct.UpdateValue(2); UpdateMyNormalStruct(myStruct); Console.WriteLine(myStruct.Value); } }

您可以猜想一下它的运行结果是什么呢? 2 还是 8?

我们来理一下,在 Main 中先调用了结构体自身的方法 UpdateValue 将 Value 修改为 2, 再调用 Program 中的方法 UpdateMyNormalStruct, 而该方法中又调用了 MyNormalStruct 结构体自身的方法 UpdateValue,那么输出是不是应该是 8 呢? 如果您这么想就错了。
它的正确输出结果是 2,这是为什么呢?

这是因为,结构体和许多内置的简单类型(sbyte、byte、short、ushort、int、uint、long、ulong、char、float、double、decimal、bool 和 enum 类型)一样,都是值类型,在传递参数的时候以值的方式传递。因此调用方法 UpdateMyNormalStruct 时传递的是 myStruct 变量的新副本,在此方法中,其实是此副本调用了 UpdateValue 方法,所以原变量 myStruct 的 Value 不会发生变化。

说到这里,有聪明的朋友可能会想,我们给 UpdateMyNormalStruct 方法的参数加上 in 修饰符,是不是输出结果就变为 8 了,in 参数不就是引用传递吗?
我们可以试一下,把代码改成:

static void UpdateMyNormalStruct(in MyNormalStruct myStruct) { myStruct.UpdateValue(8); } static void Main(string[] args) { MyNormalStruct myStruct = new MyNormalStruct(); myStruct.UpdateValue(2); UpdateMyNormalStruct(in myStruct); Console.WriteLine(myStruct.Value); }

运行一下,您会发现,结果依然为 2 !这……就让人大跌眼镜了……
用工具查看一下 UpdateMyNormalStruct 方法的中间语言:

.method private hidebysig static void UpdateMyNormalStruct ( [in] valuetype ConsoleApp4InTest.MyNormalStruct& myStruct ) cil managed { .param [1] .custom instance void [System.Runtime]System.Runtime.CompilerServices.IsReadOnlyAttribute::.ctor() = ( 01 00 00 00 ) // Method begins at RVA 0x2164 // Code size 18 (0x12) .maxstack 2 .locals init ( [0] valuetype ConsoleApp4InTest.MyNormalStruct ) IL_0000: nop IL_0001: ldarg.0 IL_0002: ldobj ConsoleApp4InTest.MyNormalStruct IL_0007: stloc.0 IL_0008: ldloca.s 0 IL_000a: ldc.i4.8 IL_000b: call instance void ConsoleApp4InTest.MyNormalStruct::UpdateValue(int32) IL_0010: nop IL_0011: ret } // end of method Program::UpdateMyNormalStruct

您会发现,在 IL_0002、IL_0007 和 IL_0008 这几行,仍然创建了一个 MyNormalStruct 结构体的防御性副本(defensive copy)。虽然在调用方法 UpdateMyNormalStruct 时以引用的方式传递参数,但在方法体中调用结构体自身的 UpdateValue 前,却创建了一个该结构体的防御性副本,改变的是该副本的 Value。这就有点奇怪了,不是吗?

我们使用 in 参数的目的就是想减少结构体的复制从而提升性能,但这里并没有起到作用。甚至,假如 UpdateMyNormalStruct 方法中多次调用该结构体的非只读方法,编译器也会多次创建该结构体的防御性副本,这就对性能产生了负面影响。

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

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