Preface
The in modifier was also introduced in C# 7.2, which is closely related to the readonly struct in C# 1 discussed in our previous article.
in Modifier
The in modifier passes arguments by reference. It makes formal parameters an alias for actual parameters, that is, any operation performed on formal parameters is performed on actual parameters. It is similar to the ref or out keywords, except that the in parameter cannot be modified by the called method.
- ref modifier, specifying that the parameter is passed by a reference and can be read or written by the calling method.
- out modifier, specifying that the parameter is passed by a reference and must be written by the calling method.
- in modifier, specifying that the parameter is passed by a reference, can be read by the calling method, but cannot be written.
Let's give a simple example:
struct Product { public int ProductId { get; set; } public string ProductName { get; set; } } public static void Modify(in Product product) { //product = new Product(); // Error CS8331 cannot be assigned to variable 'in Product' because it is a read-only variable // = "test product"; // Error CS8332 cannot be assigned to a member of the variable 'in Product' because it is a read-only variable ($"Id: {}, Name: {}"); // OK }
Reasons for introducing in parameter
We know that the memory of a structure instance is allocated on the stack, and the occupied memory is collected with the type or method that declares it, so it usually has an advantage over the reference type in memory allocation. 2
But for some large structures (such as many fields or properties), it is expensive to copy these structures when calling methods in compact loops or critical code paths. When the called method does not modify the state of the parameter, the new modifier in declare the parameter to specify that this parameter can be passed safely by reference, which can avoid (possibly incurred) high replication costs, thereby improving the performance of code running.
In Parameters to improve performance
In order to test the performance improvement of the in modifier, I defined two larger structures, one is the variable structure NormalStruct and the other is the read-only structure ReadOnlyStruct, both of which define 30 properties and then define three test methods:
- DoNormalLoop method, the parameters are not modifiers and are passed into general structures, which is a common practice in the past.
- DoNormalLoopByIn method, add in the parameter modifier, and pass it into the general structure.
- DoReadOnlyLoopByIn method, add the in modifier to the parameter, and pass it into the read-only structure.
The code looks like this:
public struct NormalStruct { public decimal Number1 { get; set; } public decimal Number2 { get; set; } //... public decimal Number30 { get; set; } } public readonly struct ReadOnlyStruct { 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) { //Business Logic return 0M; } public decimal ComputeIn(in NormalStruct s) { //Business Logic return 0M; } public decimal ComputeIn(in ReadOnlyStruct s) { //Business Logic return 0M; } }
In methods without using the in parameter, it means that each call is passed a new copy of the variable; while in methods using the in modifier, each time is not a new copy of the variable, but a read-only reference to the same copy.
Use the BenchmarkDotNet tool to test the runtime of the three methods, and the results are as follows:
| 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 |
From this result, it can be seen that if the in parameter is used, whether it is a general structure or a read-only structure, the performance will be greatly improved compared to the parameters that do not use the in modifier. This performance difference may vary on different machines, but there is no doubt that using the in parameter will result in better performance.
Use in
In the simple for loop above, we see that the in parameter helps improve performance, so what about in parallel operations? We changed the above for loop to use to implement it, the code is as follows:
[Benchmark(Baseline = true)] public decimal DoNormalLoop() { decimal result = 0M; (0, loops, i => Compute(normalInstance)); return result; } [Benchmark] public decimal DoNormalLoopByIn() { decimal result = 0M; (0, loops, i => ComputeIn(in normalInstance)); return result; } [Benchmark] public decimal DoReadOnlyLoopByIn() { decimal result = 0M; (0, loops, i => ComputeIn(in readOnlyInstance)); return result; }
In fact, the principle is the same. In the method using the in parameter, each call passes a new copy of the variable; in the method using the in modifier, each time a read-only reference of the same copy is passed.
Use the BenchmarkDotNet tool to test the runtime of the three methods, and the results are as follows:
| 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 |
It also shows that using the in parameter will achieve better performance.
What to note when using in parameters
Let's look at an example, defining a general structure, including an attribute Value and a method to modify the attribute UpdateValue. Then, define a method UpdateMyNormalStruct elsewhere to modify the property Value of the structure. The code is as follows:
struct MyNormalStruct { public int Value { get; set; } public void UpdateValue(int value) { Value = value; } } class Program { static void UpdateMyNormalStruct(MyNormalStruct myStruct) { (8); } static void Main(string[] args) { MyNormalStruct myStruct = new MyNormalStruct(); (2); UpdateMyNormalStruct(myStruct); (); } }
You can guess what its running result is? 2 or 8?
Let’s take a look. In Main, the structure’s own method UpdateValue is called first. Change Value to 2, and then the method UpdateMyNormalStruct in Program is called. In this method, the structure’s own method UpdateValue is called. So should the output be 8? If you think so, you're wrong.
Its correct output is 2, why is this?
This is because, like many built-in simple types (sbyte, byte, short, ushort, int, uint, long, uint, char, float, double, decimal, bool and enum types), structures are value types, and are passed in the form of values when passing parameters. Therefore, when calling the method UpdateMyNormalStruct, the new copy of the myStruct variable is passed. In this method, it is actually this copy that calls the UpdateValue method, so the Value of the original variable myStruct will not change.
Speaking of this, smart friends may think, if we add the in modifier to the parameters of the UpdateMyNormalStruct method, will the output result become 8? Isn’t the in parameter a reference pass?
We can try it and change the code to:
static void UpdateMyNormalStruct(in MyNormalStruct myStruct) { (8); } static void Main(string[] args) { MyNormalStruct myStruct = new MyNormalStruct(); (2); UpdateMyNormalStruct(in myStruct); (); }
Run it and you will find that the result is still 2! This...is surprising...
Use the tool to view the intermediate language of the UpdateMyNormalStruct method:
.method private hidebysig static void UpdateMyNormalStruct ( [in] valuetype & myStruct ) cil managed { .param [1] .custom instance void []::.ctor() = ( 01 00 00 00 ) // Method begins at RVA 0x2164 // Code size 18 (0x12) .maxstack 2 .locals init ( [0] valuetype ) IL_0000: nop IL_0001: ldarg.0 IL_0002: ldobj IL_0007: stloc.0 IL_0008: 0 IL_000a: ldc.i4.8 IL_000b: call instance void ::UpdateValue(int32) IL_0010: nop IL_0011: ret } // end of method Program::UpdateMyNormalStruct
You will find that in the lines IL_0002, IL_0007, and IL_0008, a defensive copy of the MyNormalStruct structure is still created. Although the parameters are passed in reference form when calling the method UpdateMyNormalStruct, a defensive copy of the structure is created before calling the UpdateValue of the structure itself in the method body, which changes the value of the copy. This is a bit strange, isn't it?
Google explained some information like this: C# cannot know whether it will also modify its value/state when it calls a method (or getter) on a structure. So, what it does is create what is called a "defensive copy". When running a method (or getter) on a structure, it creates a copy of the passed structure and runs the method on the copy. This means that the original copy is exactly the same as when it was passed in, and the value passed in by the caller has not been modified.
Is there a way to make the method UpdateMyNormalStruct output 8 after calling? You try changing the parameter to ref modifier:stuck_out_tongue_winking_eye: :grin: :joy:
To sum up, it is best not to use the in modifier with a general (non-read-only) structure to avoid obscure behavior and may have a negative impact on performance.
In parameter limitations
The in, ref, and out keywords cannot be used in the following methods:
- Asynchronous method, defined by using the async modifier.
- Iterator methods, including yield return or yield break statements.
- The first parameter of the extension method cannot have the in modifier unless the parameter is a structure.
- The first parameter of the extension method, where the parameter is a generic type (even if the type is constrained as a struct.)
Summarize
Use the in parameter to help clarify the intent of this parameter being unmodified.
When the size of the readonly struct is greater than 3, it should be passed as an in parameter for performance reasons.
Do not use general (non-read-only) structures as in parameters, because structures are variable, which may negatively affect performance and may cause obscure behavior.
This is the article about in parameters and performance analysis in C#. For more related in parameters and performance content in C#, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!