IL and Memory Allocations
.NET C#, similarly to Java, or LLVM-based languages, is a VM-based language. It gets compiled into an Intermediary Language, which is then compiled (usually JIT) into platform-specific machine code.
IL is a stack-based language. Looking through the generated IL code, we can understand how memory is being managed in our programs. Specifically, we can see how many memory allocations there are. This is quite important since lots of allocations might cause GC to hurt our app’s performance.
Below are a few examples of C# - IL snippets with notes on interesting parts. I disabled nullability for the IL snippets to be shorter.
Simple Class With One Method
The example is rather straightforward, here’re some things to note:
// [5 41 - 5 51]- such comments appear often in generated IL code (at least in Debug builds) to indicate line/column numbers of corresponding C# code.
Since IL is stack-based, we will see lots of loads (instructions starting with
LD), which are followed by operations that act on values on the stack (like
ADD). Wikipedia contains listing of all CIL instructions. Here’s a brief explanation of instructions that appear in our code example:
LDARG.1- loads first argument onto the stack (in this case it’s the value of
LDC.i4.1- pushes “1” on top of the stack (since we’re incrementing the provided
ADD- without any surprise, it adds two last values on the stack
RET- returns from a method
CALL- calls a method. In this case, since our class does not inherit from anything explicitly, it implicitly inherits
Object’s constructor is called within our class’s constructor. Before it’s called,
LDARG.0is used to load
thisonto the stack. This reference is required by the constructor.
NOP- no operation. It’s used in Debug builds, to mark points where breakpoints could be placed even though there isn’t any actual instruction (like on opening/closing braces).
Returning a Delegate From Class’s Method
LDFTN pushes a pointer to a function/method on a stack. It is required by
Func constructor (it requires a target - class instance, and functon pointer -
the function to be invoked).
Every invocation of
GetDelegate causes a new isntance of
Func to be
allocated on the heap (
NEWOBJ ...Func2<int32, int32>...). We could optimize
the C# code by storing a singleton instance of
Func and reusing it lated:
Returning a Delegate From Lambda (no closure)
The generated IL is considerably longer than before, even thought the C# code is actually shorter. A few new instructions popped up:
STSFLD <field>- pops value from a stack a puts it in a requested static field. In this case, a new isntance of compiler-generated class
<>cis created, and then its assigned to
<>9of that class.
LDSFLD <field>- pushes value of specified static field onto the stack.
BRTRUE.S <label>- jumps to the specified label if value on stack is non-zero
Let’s try to “decompile” the IL code above back into C#, together with the compiler-generated stuff:
The most insteresting thing in classes that contain lambdas is that compiler
generates additional class for us behind the scenes, and puts lamda function’s
code in that class. The generated class has a static constructor (
instead of usual
Interestingly, when using lambda, the compiler already includes the optimization
that we had to add by ourselves before. The delegate gets instantiated just
once, therefore there’s just one allocation, no matter how many times we call
Returning a Delegate From Lambda (with closure)
This time, we’re going to intrdocue a closure. The sample is similar to what we looked at previously, only this time our lambda will keep the state of the method where it was created:
The result is very similar to what we had seen before. Mostly the lamba function’s logic in the compiler-generated class is more involved, local variables are introduced.