static readonly fields
6/3/2025
In the last half of every year, something truly magical happens. No, not Christmas, but Stephen Toub's yearly preformance improvements in .NET blog. Here is last year's version, if you are interested.
Ok fine, while not being literal magic, there is always something new to learn those excellent blogs. One of the things that piqued my interest in .NET 8's article is the optimization of static readonly
fields. Once static readonly
fields are initalized, the runtime might optimize accesses to this field as if its a constant - const
. The runtime can do this because of tiering. You see, the C# compiler itself does not turn your .cs files into assembly. Instead, it's converted into IL, which is in turn converted into assembly at runtime by the dotnet JIT (Just-In-Time) compiler. In addition, hot methods can be recompiled by the JIT in a process called tiering. It is on this recompilation where the JIT can treat initalized static readonly
fields as constants.
This optimization is really interesting because it is uniquely an optimization that only a JIT compiler can do. It also gave me some ideas for some C# trickery.
Warning! Entering undefined behavior territory! The behavior shown is sensitive and may not be replicable. Obviously, don't depend this behavior for anything, everything here is just for educational purposes.
Sooo... While in safe managed code you cannot modify static readonly
fields, you can with some unsafe code.
class Static { public static readonly int Value = 42; public static unsafe void Modify(int newValue) { fixed (int* pts = &Value) *pts = newValue; } }
The inevitable conclusion being that one can "bake" constants into the assembly, then change values for some truely strange behavior.
class Program { static void Main() { Cursed(); Console.WriteLine(M1()); Console.WriteLine(M2()); } static int M1() => Static.Value; static int M2() => Static.Value; private static void Cursed() { Static.Modify(69); for (int j = 0; j < 4; j++) { // Hit the 30 call threshold for (int i = 0; i < 30; i++) { M1(); } // Give the jit some time to recompile Thread.Sleep(100); } Static.Modify(42); } }
Here, we "bake" the value of 42 (0x2A) into M1()
, while M2()
remains untouched until it is called at the end, which results in the following output.
69 42
Pretty cool, right? You can try it too. I used .NET 8 - oh, and it also needs to be in release mode.
Practicality?
This behavior alone is not practical in any way, but it is very cool to see. The applications of these kinds of "runtime constants" are very practical, however. For me, this came in handy when optimizing my ECS/ECF library, Frent. Component ids associated with some type <T>
were created at runtime and stored in static readonly
fields. These ids are used to index into arrays, and by making them static readonly
, indexing into an array becomes the same as reading a field on an object. While in most cases the benefit is minimal, in tight paths like entity.Get<T>()
precious cycles are saved where every lookup depends on the last.