This question has been bouncing around my head for a few years and I wanted to finally ask it.
The question is: how does the CLR implement static fields in generic types? Under the hood, where are they stored and how efficient is it to access them?
The reason I find this question interesting is that a program can create arbitrarily many new types at runtime -- types that were not mentioned at compile time.
So, the runtime has to stick the statics somewhere. It must be that, conceptually, there is a map from each type to its statics. The easiest way to implement this might be that the System.Type class contains some hidden Object _myStatics field. Then the runtime would need to do only one pointer dereference to get from a type to its statics, though it still would have to take care of threadsafe exactly-once initialization.
Anyway, to anyone who works on the CLR or knows how things work under the hood, does this sound about right?
I'm going to append two programs below to try to explain what I'm talking about in case I'm not making sense.
using System.Diagnostics;
public static class Program1 {
private const int Depth = 1000;
private class Foo<T>;
public static void Main() {
List<Type> list1 = [];
NoteTypes<object>(Depth, list1);
List<Type> list2 = [];
NoteTypes<object>(Depth, list2);
for (var i = 0; i != Depth; ++i) {
Trace.Assert(ReferenceEquals(list1[i], list2[i]));
}
}
public static void NoteTypes<T>(int depth, List<Type> types) {
if (depth <= 0) {
return;
}
types.Add(typeof(T));
NoteTypes<Foo<T>>(depth - 1, types);
}
}
The above program creates 1000 new distinct System.Types, stores them in a list, and then repeats the process. The System.Types in the second list are reference-equal to those in the first. I think this means that there must be a threadsafe “look up or create System.Type” canonicalization going on, and this also means that an innocent-looking recursive call like NoteTypes<Foo<T>>() might not be as fast as you otherwise expect, because it has to do that work. It also means (I suppose most people know this) that the T must be passed in as an implicit System.Type argument in much the same way that the explicit int and List<Type> arguments are. This must be the case, because you need things like typeof(T) and new T[] to work and so you need to know what T is specifically bound to.
using System.Diagnostics;
public static class Program2 {
public interface IAccessValue {
public static abstract int Value { get; set; }
}
public class Foo<T> : IAccessValue {
public static int Value { get; set; }
}
private const int Depth = 1000;
public static void Main() {
SetValues<Foo<object>>(Depth);
CheckValues<Foo<object>>(Depth);
Trace.Assert(Foo<object>.Value == Depth);
Trace.Assert(Foo<Foo<object>>.Value == Depth - 1);
Trace.Assert(Foo<Foo<Foo<object>>>.Value == Depth - 2);
Trace.Assert(Foo<bool>.Value == default);
}
public static void SetValues<T>(int depth) where T : IAccessValue {
if (depth <= 0) {
return;
}
T.Value = depth;
SetValues<Foo<T>>(depth - 1);
}
public static void CheckValues<T>(int depth) where T : IAccessValue {
if (depth <= 0) {
return;
}
Trace.Assert(T.Value == depth);
CheckValues<Foo<T>>(depth - 1);
}
}
The above program also creates 1000 fresh types but it also demonstrates that each type has its own distinct static field (here, accessed by a static property). I had to get a little funky with interfaces to make the code nice. But the four asserts in Main show that each distinct Foo has its own distinct static field.
TL;DR what’s the most clever way to implement this in the runtime to make it fast? Is it a private object field hanging off System.Type or something more clever?