我正在尝试测试添加到 C# 中的"in"关键字的性能(或不性能)。in 关键字应该能够将对值类型的只读引用传递到方法中,而不是先复制值,然后再传入。
通过绕过这个副本,in应该更快,但在我的测试中,它似乎一点也不快。
我正在使用BenchMarkDotNet来对我的代码进行基准测试。代码看起来像:
public struct Input
{
public decimal Number1 { get; set; }
public decimal Number2 { get; set; }
}
public class InBenchmarking
{
const int loops = 50000000;
Input inputInstance;
public InBenchmarking()
{
inputInstance = new Input
{
};
}
[Benchmark]
public decimal DoSomethingRefLoop()
{
decimal result = 0M;
for (int i = 0; i < loops; i++)
{
result = DoSomethingRef(ref inputInstance);
}
return result;
}
[Benchmark]
public decimal DoSomethingInLoop()
{
decimal result = 0M;
for (int i = 0; i < loops; i++)
{
result = DoSomethingIn(inputInstance);
}
return result;
}
[Benchmark(Baseline = true)]
public decimal DoSomethingLoop()
{
decimal result = 0M;
for (int i = 0; i < loops; i++)
{
result = DoSomething(inputInstance);
}
return result;
}
public decimal DoSomething(Input input)
{
return input.Number1;
}
public decimal DoSomethingIn(in Input input)
{
return input.Number1;
}
public decimal DoSomethingRef(ref Input input)
{
return input.Number1;
}
}
如您所见,我包含一个循环来使用"ref"关键字,该关键字也通过引用传递,但不是只读的。这似乎确实更快。
该测试的结果是:
Method | Mean | Error | StdDev | Scaled | ScaledSD |
------------------- |---------:|----------:|----------:|-------:|---------:|
DoSomethingRefLoop | 20.15 ms | 0.3967 ms | 0.6058 ms | 0.41 | 0.03 |
DoSomethingInLoop | 48.88 ms | 0.9756 ms | 2.5529 ms | 0.98 | 0.08 |
DoSomethingLoop | 49.84 ms | 1.0872 ms | 3.1367 ms | 1.00 | 0.00 |
因此,使用"in"似乎一点也不快。我觉得某些东西可能正在以我意想不到的方式进行优化,这解释了性能差异。我尝试将结构的大小增加到 16 个小数字段,但同样,它在 in 和 by 值之间没有区别。
如何构建基准测试以真正了解 in、ref 和按值传递之间的差异?
问题是您使用的是非readonly
结构,因此编译器正在DoSomethingIn
方法中创建输入参数的防御性副本。
发生这种情况是因为您使用的是Number1
属性的 getter 方法,并且编译器不确定结构状态是否会因此而更改(并且由于参数作为只读引用传递,因此无效)。
如果像这样编辑结构:
public readonly struct Input
{
public decimal Number1 { get; }
public decimal Number2 { get; }
}
再次运行基准测试,in
方法将获得与ref
方法相同的性能,这是您最初的假设。
注意:readonly struct
修饰符不是强制性的,您也可以通过直接公开字段来解决此问题,如下所示:
public struct Input
{
public decimal Number1;
public decimal Number2;
}
关键是,正如这里所说,:
编译器无法知道是否有任何成员方法修改了结构的状态。为了确保不修改对象,编译器会创建一个副本并使用该副本调用成员引用。任何修改都是对该防御性副本的。
编辑#2:为了进一步澄清为什么需要readonly struct
修饰符(同样,in
与ref readonly
相同),这是文档中的另一段:
[...]其他时候,您可能希望创建一个不可变的结构。然后,您始终可以通过只读引用传递。这种做法会删除在访问用作 in 参数的结构的方法时发生的防御性副本。