You want to take a look into how C# arrays are compiled into MSIL opcodes. This increases your knowledge of the .NET framework and can help you make better decisions about types of arrays to use. It also can improve performance.
When you assign elements in an array in C#, a series of events occurs. The array itself is pushed onto the stack. Next the index you are using in the array is pushed onto the stack, and the value you want to put into the array is pushed onto the stack.
Those three items are popped from the stack and finally the value is placed into the array element. This is the theory, but what does this look like in the MSIL?
Here I show three complete C# programs and the intermediate language they are compiled into. The type-specific opcodes are highlighted on the right. Each program uses a different type of array.
| C# program - before compilation | MSIL instructions - after compilation |
| class Program { static void Main() { char[] c = new char[100]; c[0] = 'f'; } } | .method private hidebysig static void Main() cil managed { .entrypoint .maxstack 3 .locals init ( [0] char[] c) L_0000: ldc.i4.s 100 L_0002: newarr char L_0007: stloc.0 L_0008: ldloc.0 L_0009: ldc.i4.0 L_000a: ldc.i4.s 0x66 L_000c: stelem.i2 L_000d: ret } |
| class Program { static void Main() { int[] i = new int[100]; i[0] = 5; } } | .method private hidebysig static void Main() cil managed { .entrypoint .maxstack 3 .locals init ( [0] int32[] i) L_0000: ldc.i4.s 100 L_0002: newarr int32 L_0007: stloc.0 L_0008: ldloc.0 L_0009: ldc.i4.0 L_000a: ldc.i4.5 L_000b: stelem.i4 L_000c: ret } |
| class Program { static void Main() { uint[] u = new uint[100]; u[0] = 2; } } | .method private hidebysig static void Main() cil managed { .entrypoint .maxstack 3 .locals init ( [0] uint32[] u) L_0000: ldc.i4.s 100 L_0002: newarr uint32 L_0007: stloc.0 L_0008: ldloc.0 L_0009: ldc.i4.0 L_000a: ldc.i4.s 0x66 L_000c: stelem.i4 L_000d: ret } |
| • 3 programs • Each program uses a different value type for its array | • First example uses 'stelem.i2' • Second example uses 'stelem.i4' • Third example uses 'stelem.i4' also |
Microsoft states that the stelem opcode in intermediate language "replaces the array element at a given index with the value on the evaluation stack, whose type is specified in the instruction."
What that means is that when you assign an element in an array, the stelem instruction is generated to actually do the work.
| Opcode | Its usage |
| OpCodes.Stelem_I2 | "Replaces the array element at a given index with the int16 value on the evaluation stack." This means that an array is assigned a char value. |
| OpCodes.Stelem_I4 | Same as above, but uses the int32 data type (which is represented by the plain 'int'). |
| OpCodes.Ldelem_I2 | "Loads the element with type int16 at a specified array index onto the top of the evaluation stack as an int32." |
| OpCodes.Ldelem_I4 | Same as above, but uses the 32-bit int size (represented by 'int'). |
The first two rows in the table show the stelem instructions, which set array elements to a value, and the second two show the ldelem instruction variants that read in values from an array element.
I wanted to know if the opcodes perform differently. My research with numeric types, and my previous reading on data types in C, indicated that the int32 data type is the fastest to use. [C# - Numeric Types and Casts - dotnetperls.com]
The below graph shows the timings of three loops that assign values to different arrays. The bodies of the loops correspond to the first three examples in this document.
My results show that setting an element in a char array is much slower than an int32 array, and the uint32 array was slighter faster still. This fits with my knowledge from C and lower-level languages. An old optimization trick is to use unsigned integers instead of signed ones.
I was not able to find a measurable performance difference between the ldelem opcodes, but I suspect that the same trend may apply there too. I was surprised the difference even on the micro-benchmark here was this big.
The results may be different. Most of Intel's Core 2 products are 32-bit size, but some Pentiums, Pentium Ds, and other chips are 64-bit. Running this benchmark 5 years from now, in 2013, would be interesting.
Yes. However, you might have a different emphasis than I do. I feel that understanding the ins and outs of MSIL is very important for an expert .NET developer. In fact, I feel it is essential.
We saw the stelem opcodes and discovered that they have performance differences. This gives us insight into our computers' microarchitectures.
Diagnosing problems in MSIL has far greater utility than the benchmarks here. If you have a really difficult problem, then understanding what opcodes like conv, stelem, and ldelem do is important.