Managed C++ codegen for new, array manipulation, delete

Managed C++ (MC++) code generation is a cool accomplishment. I think it's another good testimony to the CLR's cross-language charter (and IL's flexibility) that we can support C++.
(Not too mention our support for more dynamic languages like F#, SML.Net, and IronPython that also compile to IL)

As the most basic example, I find it interesting to compare the IL generated for a simple program that allocates a buffer and does some manipulation when written in MC++ and C#.

MC+++ C#
#include "stdafx.h"
using namespace System;

int main()
"Multiplication table");

int size = 10;
int * table= new int [size];

for(int i = 0; i < size; i ++)
table[i] = i * i;
// unsafe pointer math!

delete [] table; // explicit free memory

using System;
class Program
static void Main(string[] args)
Console.WriteLine("Multiplication table");

int size = 10;
int [] table= new int [size];

for(int i = 0; i < size; i++)
            table[i] = i * i;
// table will be freed by GC

The thing I find that causes a double-take for the most people is how MC++ can have explicit memory management (eg, "new" and "delete") and pointer arithmetic when it runs on the CLR; which does garbage collection (instead of explicit memory management) and safe references (instead of raw pointers). 
The programs do the same thing (initialize a table of squares). Here's a side-by-side comparison of the IL (with source inline) between the C# and MC++. I've made some annotations and shaded the rows corresponding to interesting differences.

description MC++ C#
declare function
and locals
.method assembly static int32 modopt([mscorlib]System.Runtime.CompilerServices.CallConvCdecl)
        main() cil managed
  .vtentry 1 : 1
  // Code size       73 (0x49)
  .maxstack  3
  .locals ([0] int32 i,
           [1] int32* table,
           [2] uint32 V_2,
           [3] int32 size,
           [4] int32 V_4)
.method private hidebysig static void  Main(string[] args) cil managed
  // Code size       47 (0x2f)
  .maxstack  4
  .locals init ([0] int32 size,
           [1] int32[] table,
           [2] int32 i,
           [3] bool CS$4$0000)

initialization   IL_0000:  ldc.i4.0
  IL_0001:  stloc.s    V_4
//000006:     {
  IL_0000:  nop
Write into //000006:     Console::WriteLine(L"Multiplication table");
  IL_0003:  ldstr      "Multiplication table"
  IL_0008:  call       void [mscorlib]System.Console::WriteLine(string)
//000007:         Console.WriteLine("Multiplication table");
  IL_0001:  ldstr      "Multiplication table"
  IL_0006:  call       void [mscorlib]System.Console::WriteLine(string)
  IL_000b:  nop
assign size=10 //000008:     int size = 10;
  IL_000d:  ldc.i4.s   10
  IL_000f:  stloc.3
//000009:         int size = 10;
  IL_000c:  ldc.i4.s   10
  IL_000e:  stloc.0
allocate table //000009:     int * table= new int [size];
  IL_0010:  ldloc.3 // size
  IL_0011:  stloc.2  // V_2
  IL_0012:  ldloc.2  // V_2
  IL_0013:  ldc.i4     0x3fffffff
  IL_0018:  bgt.un.s   IL_001f
  IL_001a:  ldloc.2 // V_2
  IL_001b:  ldc.i4.4
  IL_001c:  mul
  IL_001d:  br.s       IL_0020
  IL_001f:  ldc.i4.m1
// we've computed number of bytes
// to allocate (size * sizeof(int)).
// We now pinvoke to: msvcr80*.dll!operator new(unsigned int)
// The return result is raw pointer value (a void *) just like in C++.
  IL_0020:  call       void* modopt([mscorlib]System.Runtime.CompilerServices.CallConvCdecl) new(uint32)
  IL_0025:  stloc.1  // storing return result as raw pointer into table
//000010:         int [] table= new int [size];
  IL_000f:  ldloc.0 // size
// have GC allocate array of Int32.
// return result is a managed object reference
  IL_0010:  newarr     [mscorlib]System.Int32
  IL_0015:  stloc.1  // store reference into table.
begin for //000011:     for(int i = 0; i < size; i ++)
  IL_0026:  ldc.i4.0
  IL_0027:  stloc.0
  IL_0028:  br.s       IL_002e
  IL_002a:  ldloc.0 // jump target for loop back
  IL_002b:  ldc.i4.1
  IL_002c:  add
  IL_002d:  stloc.0
  IL_002e:  ldloc.0 // jump target
  IL_002f:  ldloc.3
  IL_0030:  bge.s      IL_003d // conditional exit at loop termination
//000012:     {
//000012:         for(int i = 0; i < size; i++)
  IL_0016:  ldc.i4.0
  IL_0017:  stloc.2
  IL_0018:  br.s       IL_0026
//000013:         {
  IL_001a:  nop
array index
and assignment
//000013:         table[i] = i * i; // unsafe pointer math!
  IL_0032:  ldloc.1  // stack: table
  IL_0033:  ldloc.0  // stack: table, i
  IL_0034:  ldc.i4.4  // stack: table, i, 4      (note 4 =sizeof(int))
  IL_0035:  mul   // stack: table, (i*4)
  IL_0036:  add  // stack: (table + i*4)
  IL_0037:  ldloc.0  // stack: (table + i*4), i
  IL_0038:  dup // stack: (table + i*4), i, i
  IL_0039:  mul  // stack: (table + i*4), (i*i)
// stind.i4 will assign 1st (top-most) stack arg, as i4 sized data,
// to address specified by 2nd stack arg.
  IL_003a:  stind.i4 // blind pointer assignment, no boundary checks.
//000014:             table[i] = i * i;
  IL_001b:  ldloc.1 // stack: table
  IL_001c:  ldloc.2 // stack: table, i
  IL_001d:  ldloc.2 // stack: table, i , i
  IL_001e:  ldloc.2 // stack: table, i, i, i
  IL_001f:  mul // stack: table, i, (i * i)
// stelem.i4 will assign 1st (top-most) stack arg to index
// from 2nd stack arg into array specified by 3rd arg.
  IL_0020:  stelem.i4 // explicit array assignment, allows boundary checks.
end for //000014:     }   
  IL_003b:  br.s       IL_002a
//000015:         }   
  IL_0021:  nop
//000012:         for(int i = 0; i < size; i++)
  IL_0022:  ldloc.2
  IL_0023:  ldc.i4.1
  IL_0024:  add
  IL_0025:  stloc.2
  IL_0026:  ldloc.2 // jump target from loop start
  IL_0027:  ldloc.0
  IL_0028:  clt
  IL_002a:  stloc.3
  IL_002b:  ldloc.3
  IL_002c:  brtrue.s   IL_001a // loop back to continue
free table //000016:     delete [] table;
  IL_003d:  ldloc.1
  IL_003e:  call       void modopt([mscorlib]System.Runtime.CompilerServices.CallConvCdecl) delete(void*)
//000016:         // table will be freed by GC
end of function
and return
//000017:     return 0;
IL_0043: ldc.i4.0
IL_0044: stloc.s V_4
//000018: }
IL_0046: ldloc.s V_4
IL_0048: ret
} // end of method 'Global Functions'::main
//000017:     }
//000018: }
IL_002e: ret
} // end of method Program::Main

For trivia points, I'll point out there are some minor cosmetic differences including:
- they generate the for-loop differently.  MC++ has the comparison first; C# has the comparison at the end. You can verify for yourself they're result in the same behavior.
- C# places a sequence point on the opening '{', whereas MC++ doesn't. Practically, this means when you first step into 'main', in C# you stop at '{', where as in MC++, you stop at the first line after '{'.
- MC++ needs to distinguish between managed and unmanaged strings. (update: MC++ 2005 will use context to decided, see section 5.1 in the translation guide.) Unmanaged strings are just a pointer (RVA) address to the raw string data (as they are in C++). In MC++, managed strings get prefixed with 'S', (like  S"Hello"). Managed strings are loaded with the 'ldstr' opcode.  Since all strings in C# are managed, C# doesn't need a special string prefix.
There are also some differences that help MC++ interoperate better with native code (like using the Cdecl calling convention on Main and that .vtentry thing).

For now, the interesting differences are around allocating 'table', doing the assignment "table[i] = i *i", and freeing the memory. I've shaded the rows corresponding to these actions.

The declaration of the 'table' local:
It turns out the languages compile the variable 'table' differently:
- MC++ compiles it to a "int32 *", which the runtimes views as a raw pointer. This is essentially opaque data to the CLR and may as well be a pointer-sized int.  (It corresponds to a CorElementType of ELEMENT_TYPE_PTR)
- C# compiles it to "int32[]", which the runtimes views as a managed array. This is a managed reference owned by the CLR, and lives on the GC heap. (It corresponds to a CorElementType of ELEMENT_TYPE_SZARRAY).

One significant consequence of this is debuggability. The native debugger can't do much with inspecting a int32* because it's just a raw pointer. It doesn't know it's actually pointing to an array. And it sure doesn't know how big that array is, so it couldn't display the bounds even if it wanted to. In contrast, an int32[] is far more descriptive to the debugger because managed arrays have rich bound information (see ICorDebugArrayValue in CorDebug.idl). Thus in the C# case, you can expand 'table' and see the full contents.

Allocating the buffer:
C# uses the "newarr" IL opcode to initial 'table'. Since 'table' is a managed array, and 'newarr' allocates a managed array, that should all make sense.
In contrast, MC++ calls a method 'new' which will eventually pinvoke out to msvcr80*.dll!operator new(unsigned int), and that will return a raw pointer value to allocated memory, just as you'd expect in unmanaged C++. You can step into the call in VS and see for yourself that you land in msvcr80!new.  It's the same story with the call to delete.  This is all consistent with MC++'s 'table' being a raw pointer.

Making assignments:
In the C# case, we've got a strongly typed array, and stelem.* is an il opcode for explicitly assigning into an array. This opcode has the semantics to let the jit do array boundary checks.
In the MC++ case, we're dealing with raw pointers. The stind.* IL opcode allows direct memory assignment.  You can see the raw pointer manipulation, just as we'd have in unmanaged C++, for the pointer arithmetic  It's still IL, but it's not verifiable. That means the code loses a lot of the protection, such as the explicit boundary checks from the stelem opcode.

C#  has an unsafe code regions which lets it do the same pointer operations that MC++ is doing here.

In conclusion:
While this is perhaps a laughably basic example (especially in retrospect), these sorts of qualities for MC++ codegen are a preview of the sort of techniques MC++ uses to codegen more complex constructs (like multiple inheritance, which is not supported in C#).

Comments (10)

  1. vista says:

    Good explanation and it has solved some of my questions too

  2. Mike Dunn says:

    The C++ prefix for a managed string is S, no? Or does the compiler deduce that it should be managed by the context?

  3. When MC++ compiles an &quot;unmanaged&quot; class into IL (from IJW), it actually compiles the class into an opaque…

  4. Mike Dunn – you’re correct on both accounts. ‘S’ is the prefix for managed strings. ‘L’ is the prefix for unicode. Whidbey MC++ does the implicit conversion (managed strings are unicode so this conversion is legal).

    I’ll update the issue above.

  5. Rick Byers says:

    It might be interesting to note that most of the differences here come from using a blob of unmanaged memory vs using a managed array (which are two very different things). Technically, this isn’t really a difference between MC++ and C#. Both languages can use both types of arrays.

    For example, I found it interesting to change your C++ example to use the line "array<int>^ table= gcnew array<int>(size);" (which is the equivalent of the C# code) and then compare the codegen with C#.

  6. Yes. however, these differences become more significant as soon as you start doing more interesting things like pointer arithmetic (which could be common when allocating a buffer) or multiple inheritence.

    I view MC++’s codegen here as the first baby step in how the rest of IJW works.

Skip to main content