The new in
keyword (for parameters) in C# 7.2 promises to make code faster:
When you add the
in
modifier to pass an argument by reference, you declare your design intent is to pass arguments by reference to avoid unnecessary copying.
However, naïve use of this modifier will result in more copies (and slower code)!
This side-effect is implied by the MSDN documentation:
You can call any instance method that uses pass-by-value semantics. In those instances, a copy of the
in
parameter is created.
It’s also mentioned in passing in the readonly ref
proposal:
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.
Consider the example method from 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);
}
And assume this implementation of 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; }
}
A number of C# features now combine in an unfortunate way:
in
parameter is readonly
readonly
struct makes a copy
this
, a copy has to be made to ensure the readonly
value isn’t modifiedEvery time a property on an in
parameter is accessed in CalculateDistance
, the compiler
has to defensively create a temporary copy of the parameter.
We’ve now gone from avoiding one copy per argument (at the call site) to three copies per argument
(inside the method body)!
This is not a new problem; see Jon Skeet’s post on The Surprising Inefficiency of Readonly Fields.
But using in
makes it a much more common problem.
The solution is also in C# 7.2: readonly struct
.
If we change public struct Point3D
to public readonly struct Point3D
(the implementation doesn’t
have to change because all fields are already readonly
), then the compiler knows it can elide
the temporary copy inside the body of CalculateDistance
. This makes the method faster than
passing the structs by value.
Note that we could have achieved the same effect in C# 7.1 by passing the struct by ref
. However,
this allows the caller to mutate its fields (if it’s mutable) or reassign the entire variable to a new
value. Using in
expresses the intent that the caller will not modify the variable at all (and the
compiler enforces that).
I’ve created a test harness that benchmarks the various
combinations of in
, ref
, struct
and readonly struct
. (Note
that I increased the struct size to 56 bytes to make the differences more obvious; smaller structs
may not be impacted as much.) The full benchmark results are in that repo;
the summary is:
Method | Mean |
---|---|
PointByValue | 25.09 ns |
PointByRef | 21.77 ns |
PointByIn | 34.59 ns |
ReadOnlyPointByValue | 25.29 ns |
ReadOnlyPointByRef | 21.78 ns |
ReadOnlyPointByIn | 21.79 ns |
in
to express design intent (instead of ref
), be aware that there may be a slight performance penalty when passing large structs.in
to avoid copies and improve performance, only use it with readonly struct
.Posted by Bradley Grainger on December 07, 2017