这个拖后腿的“in”
问题之源
C# 7.2推出了全新的参数修饰符in,据说是能提升一定的性能,官方MSDN文档描述是:
Add the
in
modifier to pass an argument by reference and declare your design intent to pass arguments by reference to avoid unnecessary copying.
然而,想当然地使用它却导致更多的副本出现,影响代码运行速度。
MSDN中还有一段隐含的副作用的描述:
You can call any instance method that uses by value parameters. In those instances, a copy of the
in
parameter is created.
同时文档也提到了readonly ref
的目的:
After adding support for
in
parameters andref redonly
[sic] returns the problem of defensive copying will get worse since readonly variables will become more common.
来看一下MSDN里的这个例子::
private static double CalculateDistance(in Point3D point1, in Point3D point2) { double xDifference = point1.X - point2.X; double yDifference = point1.Y - point2.Y; double zDifference = point1.Z - point2.Z; return Math.Sqrt(xDifference * xDifference + yDifference * yDifference + zDifference * zDifference); }
假设Point3D类型是这样定义的:
public struct Point3D { public Point3D(double x, double y, double z) { X = x; Y = y; Z = z; } public double X { get; } public double Y { get; } public double Z { get; } }
结果C#的几个本不相关的特性以一种闹心的方式结合起来:
- 标记了
in
的结构体参数是readonly只读的 - 调用标记为readonly的结构体的实例化方法将产生一个副本
- 因为这个方法要通过改变this指针来达到确保标记了
readonly
的原值不会被修改
- 因为这个方法要通过改变this指针来达到确保标记了
- 属性访问器也是实例方法,受this影响
每次给CalculateDistance方法传递标记了in的结构体参数时,编译器会在访问时自动为这个参数的每个属性创建一个副本,本以为不会创建副本,结果反而每个传进来的参数在方法内部弄出来3个!
这个问题存在已久,看一下Jon Skeet的博客:The Surprising Inefficiency of Readonly Fields。只不过使用in
让这个尴尬的场面更频繁易现了。
解决方案
解决办法同样来自C# 7.2:readonly struct
.
如果将public struct Point3D改成
public readonly struct Point3D
,因为所有字段也已经是readonly了,所以整个结构体都无需改变,编译器此时也会省掉副本的操作,只有这样才会出现结构体参数比按值传递获得更快的运行速度。
不过注意在C# 7.1中结构体是可以通过标记ref
来传参达到同样避免副本开销的。尽管如此,结构体参数的字段想要改变值仍是可变的,甚至这个结构体都可以指向另一个新的。在函数的参数列表中使用in
的声明主要意图还是为了告诉调用者本函数不会去修改传进来的参数,当然编译器也会配合强制保证。
示例
这里有一个关于in
, ref
, struct
和readonly struct各种组合的性能测评(结构体size太小看不出差别,因此这个示例把结构体增加到56 bytes以便跑出更明显的对比效果)。结果如下:
总结
- 当使用
in
代替ref表示设计意图时,要明白在传递较大且较多的结构体时会有微小的性能损失 - 当使用
in
又要避免产生副本或提高性能,在声明结构体时要使用readonly struct