What simplifications are made when compiling MSIL code to some specific machine? I previously thought that machine code has no stack based operations, and that all the stack based operations in MSIL are converted into numerous data movement operations that have the desired push/pop stack outcome, and as a result machine code is generally far longer than MSIL code. But this doesn't seem to be the case, so it makes me wonder - just how different is machine code to MSIL code, and in what aspects?
I'd appreciate a comparison of the two from different perspectives, such as: How does the amount of operations/instructions differ? Does machine code generally have lot more lines? What else besides platform independence (at least in the sense of cpu architecture independence, and windows based platforms independence), metadata-style code, and being some sort of "common ground"language for numerous high level programming languages, does the intermediate/MSIL code allow? What might be the most noticeable differences if one compared some MSIL code and the corresponding machine code?
I'd really appreciate a mostly high level comparison, but perhaps with some easy and concrete examples.
First of all, let's assume that “machine code” means x86-64
instruction set. With other architectures such as ARM
particular aspects may be slightly different.
What simplifications are made when compiling MSIL code to some specific machine?
These are not really simplifications. MSIL and a typical machine instruction set such as x86-64` are principally different.
I previously thought that machine code has no stack based operations, and that all the stack based operations in MSIL are converted into numerous data movement operations that have the desired push/pop stack outcome, and as a result machine code is generally far longer than MSIL code.
The stack is a core concept practically necessary by every CPU architecture (there are / were some CPU architecture without a stack, but I think that's a rather rare case). A lot of operations would be impractically complicated without a working stack.
However: The primary concept in hardware CPUs are registers. Most computations and memory operations can happen in purely in registers instead of in the computer's main memory. Think of them as temporary variables. In addition, they are much, much faster to work with than with the main memory (even despite all the levels of caches in between).
That being said, while MSIL instructions must obey a purely-stack based approach to working with data (there are no registers in MSIL), with hardware CPUs it is necessary to use registers. So this results in two different approaches to translating the same expression into the respective machine code.
But this doesn't seem to be the case, so it makes me wonder - just how different is machine code to MSIL code, and in what aspects?
Let's have the C# expression: a = b + c * d;
, where each variable is an int.
In MSIL:
ldloc.1 // b — load from local variable slot 1
ldloc.2 // c — load from local variable slot 2
ldloc.3 // d — load from local variable slot 3
mul // multiple two top-most values, storing the result on the stack
add // add two top-most values, storing the result on the stack
stloc.0 // a — store top-most value to local variable slot 0
One big advantage of this concept is that it's very easy to write a code generator for pure stack-based machine code.
In x86-64
assembly:
mov eax, dword ptr [c] // load c into register eax
mul dword ptr [d] // multiply eax (default argument) with d
add eax, dword ptr [b] // add b to eax
mov dword ptr [a], eax // store eax to a
As you can see, in this simple case there's no stack involved in x86-64
. The code looks also shorter and maybe more readable. However, generating real x86-64
machine code is a very hard task.
Disclaimer: I wrote the assembly code snippet by hard; pardon my mistakes that it may contain. It's not my everyday job to write assembly these days :)
How does the amount of operations/instructions differ?
The answer is: it depends. Some simple operations such as arithmetic operations are sometimes 1:1, e.g. an add
in MSIL can result into a single add
in x86-64
. On the other hand, MSIL can take the advantage of defining much more higher-level operations. For example, the MSIL instruction callvirt
which invokes a virtual method, does not have a simple counterpart in x86-64
: you'd need several instructions to perform that call.
Does machine code generally have lot more lines?
I have to hard data available to compare; however, as per the above regarding complexity of instructions, I'd say rather yes.
What else besides platform independence and metadata-style code does the intermediate/MSIL code allow?
I think the question should rather be: what else does machine code allow? MSIL is rather restrictive. The CLR defines a lot of rules that help maintain consistency and correctness of MSIL code. In machine code you have total freedom—and you can totally mess things up as well.
What might be the most noticeable differences if one compared some MSIL code and the corresponding machine code?
From my perspective, it's the register-based architecture of CPUs such as x86-64
.
What does MSIL make easy besides these features? What are some natural structures/features of the MSIL language that make some things easier?
In fact, there are many. First of all, being a stack-based architecture, it's much easier to compile a .NET programming language into MSIL, as I've explained earlier. Then there are many other smaller things, such as:
newobj
), call methods including virtual method calls (very important)