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 with 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.
IL Examples
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 (likeADD
). 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 ofnumber
argument).LDARG.0
would representthis
.LDC.i4.1
- pushes “1” on top of the stack (since we’re incrementing the providednumber
)ADD
- without any surprise, it adds two last values on the stackRET
- returns from a methodCALL
- calls a method. In this case, since our class does not inherit from anything explicitly, it implicitly inheritsObject
, andObject
’s constructor is called within our class’s constructor. Before it’s called,LDARG.0
is used to loadthis
onto 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<>c
is created, and then its assigned to<>9
of 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 (cctor
instead of usual ctor
).
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
GetDelegate
.
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:
Here’s IL:
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.