In this blog post we will discover whether or not the readonly
modifier can make your code faster. So without further ado let's get started.
Update 24th of April: I originally used the DEBUG
build for IL/JIT code, which is obviously never a good idea when you talk about performance.I did correct the blog post. Thanks @lassevk for noticing!
What is the readonly
modifier?
The readonly
modifier is a keyword in C# that can be applied to fields. When a field is marked as readonly
it means that the field can only be assigned a value during the declaration or in the constructor of the class. Once the field has been assigned a value it cannot be changed.
An example:
public class Class
{
private readonly int readonlyField = 100;
private int nonReadonlyField = 100;
public int RO() {
return readonlyField + readonlyField;
}
public int NonRO() {
return nonReadonlyField + nonReadonlyField;
}
}
Does readonly
make your code faster?
To answer that question we have to look at the IL code that is generated by the C# compiler. Let's take a look at the IL code for the RO
method and the NonRO
method.
.class public auto ansi beforefieldinit Class
extends [System.Runtime]System.Object
{
// Fields
.field private initonly int32 readonlyField
.field private int32 nonReadonlyField
// Methods
.method public hidebysig
instance int32 RO () cil managed
{
// Method begins at RVA 0x2050
// Code size 14 (0xe)
.maxstack 8
IL_0000: ldarg.0
IL_0001: ldfld int32 Class::readonlyField
IL_0006: ldarg.0
IL_0007: ldfld int32 Class::readonlyField
IL_000c: add
IL_000d: ret
} // end of method Class::RO
.method public hidebysig
instance int32 NonRO () cil managed
{
// Method begins at RVA 0x205f
// Code size 14 (0xe)
.maxstack 8
IL_0000: ldarg.0
IL_0001: ldfld int32 Class::nonReadonlyField
IL_0006: ldarg.0
IL_0007: ldfld int32 Class::nonReadonlyField
IL_000c: add
IL_000d: ret
} // end of method Class::NonRO
.method public hidebysig specialname rtspecialname
instance void .ctor () cil managed
{
// Method begins at RVA 0x206e
// Code size 23 (0x17)
.maxstack 8
IL_0000: ldarg.0
IL_0001: ldc.i4.s 100
IL_0003: stfld int32 Class::readonlyField
IL_0008: ldarg.0
IL_0009: ldc.i4.s 100
IL_000b: stfld int32 Class::nonReadonlyField
IL_0010: ldarg.0
IL_0011: call instance void [System.Runtime]System.Object::.ctor()
IL_0016: ret
} // end of method Class::.ctor
} // end of class Class
The methods are identical, well except for the field they are accessing. The only difference is int the declaration of the fields. The readonlyField
is declared as initonly
and the nonReadonlyField
is not. The initonly
keyword is used to mark a field as readonly (basically readonly
in IL terms).
Now, there could be potentially some performance benefits to using readonly
fields. The JIT compiler could potentially optimize the code better if it knows that a field is readonly. But in this case, the JIT compiler does not optimize the code differently. The code generated for the RO
method and the NonRO
method is identical.
Here some JIT code:
Class.RO()
L0000: mov eax, [ecx+4]
L0003: add eax, eax
L0005: ret
Class.NonRO()
L0000: mov eax, [ecx+8]
L0003: add eax, eax
L0005: ret
It is again completely identical. The JIT compiler does not optimize the code differently if a field is marked as readonly
(in this case). I also tried this with more complex objects/classes instead of value types, but the result was the same. I couldn't find a way to make the JIT compiler optimize the code differently if a field is marked as readonly
.
If I run a benchmark with something like the above, you get what you would expect in this case - the somewhat same runtime:
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
BenchmarkRunner.Run<ManyFieldsBenchmark>();
public class ManyFieldsBenchmark
{
private class ReadOnlyFields
{
public readonly int Field1 = 1;
public readonly int Field2 = 2;
public readonly int Field3 = 100;
public readonly int Field4 = 300;
public readonly int Field5 = 300;
public readonly int Field6 = 300;
}
private class NonReadOnlyFields
{
public int Field1 = 1;
public int Field2 = 2;
public int Field3 = 100;
public int Field4 = 300;
public int Field5 = 300;
public int Field6 = 300;
}
private readonly ReadOnlyFields readOnlyInstance = new();
private readonly NonReadOnlyFields nonReadOnlyInstance = new();
[Benchmark]
public int AccessReadOnlyFields()
{
var sum = 0;
for (var i = 0; i < 10000; i++)
{
sum += readOnlyInstance.Field1
+ readOnlyInstance.Field2
+ readOnlyInstance.Field3
+ readOnlyInstance.Field4
+ readOnlyInstance.Field5
+ readOnlyInstance.Field6;
}
return sum;
}
[Benchmark]
public int AccessNonReadOnlyFields()
{
var sum = 0;
for (var i = 0; i < 10000; i++)
{
sum += nonReadOnlyInstance.Field1
+ nonReadOnlyInstance.Field2
+ nonReadOnlyInstance.Field3
+ nonReadOnlyInstance.Field4
+ nonReadOnlyInstance.Field5
+ nonReadOnlyInstance.Field6;
}
return sum;
}
}
| Method | Mean | Error | StdDev |
|------------------------ |---------:|---------:|---------:|
| AccessReadOnlyFields | 17.58 us | 0.100 us | 0.094 us |
| AccessNonReadOnlyFields | 17.52 us | 0.052 us | 0.046 us |
We are in the margins of error. So if there is any performance benefit to using readonly
fields, it is negligible in almost all cases. But then again, the main reason of using readonly
fields is to make your code more robust and maintainable, not to make it faster. If you suspect that it can bring benefits then
- Measure it (create a baseline), implement the change and measure it again (against your baseline).
- Inform me, as I would be interested in seeing a case where it does make a difference.