Introduction
In this, the first part of a three part series covering Common Intermediate Language (CIL) we will cover: the purpose of CIL, the role CIL plays in both .NET and Mono; compilation strategies; the two instruction sets that CIL comprises of; and verification of the CIL stream. The rest of the series is structured as follows: Part 2 covers the makeup of a managed module; and Part 3 explores the base and object model instruction sets.
This series is by no means a comprehensive guide to CIL, as such the reader is not expected to walk away and be a subject expert. However, the reader can expect to gain a general understanding of the key instructions that CIL is composed of, and more importantly, where CIL fits in within the .NET/Mono ecosystems. The series takes a relaxed approach to the subject. The reader does not need to be particularly well versed with C#, or CIL itself; however, it will help.
The purpose of CIL
Our first job is to define the role of CIL. Simply, CIL is a high-level abstract instruction set. CIL is not bound to any specific CPU architecture; as such it is portable (provided there exists the mechanics to generate native instructions for the host CPU architecture, more on this later). All compilers that target .NET or Mono generate managed modules whose managed code sections consist of CIL. (I'm keeping this overly simple at the moment. Managed compilers actually generate an assembly, an assembly consists of one or more managed modules.)
CIL and .NET
The role that CIL plays within .NET and Mono is identical from a fairly high-level perspective. However, there are some differences between the two internally.
Note: The material within this section is based on the code from the Shared Source CLI (SSCLI) 2.0 implementation, therefore it may not be entirely accurate for the commercial, closed source implementation of the CLR.
When one executes a managed program using Microsoft's CLR the CIL instruction stream is translated into native code at runtime (a more thorough discussion of compilation can be found later). This is the case with the SSCLI; CIL is the one and only intermediate representation of the users program. Just before a method is executed the CIL associated with that method is translated into the appropriate native code, the native image for the method is then cached. Further calls to the method will use the native stub of the method, rather than the CIL stub. In some cases a method will have to be JIT compiled again, but this is only when memory constraints dictate, i.e. there is not enough memory to store all the native images.
CIL and Mono
Just like the SSCLI, Mono also translates CIL code to native code at runtime. Internally Mono does not work with the CIL representation of the program. Rather, the CIL is translated into an intermediate representation (IR). The form that the IR takes is specific to Mono and all optimization routines operate exclusively on IR and not CIL. In effect, Mono has a further step than that of the SSCLI. First the user compiles their program to CIL, then when the user executes their program using the mono application launcher the CIL is translated to IR at runtime. A varying number of optimizations then occur on this IR at runtime, and native code is then produced.
Compilation
In the previous two sections we have assumed that the program has been just-in-time (JIT) compiled to native code at runtime, which is typically the way in which a program is compiled unless explicitly stated otherwise. As well as JIT compilation there exists another form of compilation: ahead-of-time (AOT) compilation. AOT compilation compiles the CIL code within your managed program to native code at compile-time rather than at runtime. The net effect of using AOT compilation is that you effectively rid yourself of the overhead that is implicit to compiling CIL to native code at runtime.
Before we progress we must first identify why both forms of compilation exist in .NET as both have pros and cons. The compilation strategy you chose will most likely be bound to the startup requirements of your application.
JIT compilation, which is the default compilation strategy has the advantage of knowing the state of the system at runtime, thus it can perform optimizations that require knowledge of the system state (e.g., whether some condition is always false, reduce incorrect branch predictions etc.). When using the JIT facility the emphasis is on speed of compilation. If JIT compilation were not fast then noticeable pauses would be observed, at least initially, when executing a managed executable.
AOT compilation translates the CIL within your program to native code at compile-time. AOT compilation is generally only used when one needs a faster startup time. For example, when loading a managed program we already incur various startup costs, e.g. we need to load the appropriate version of the CLR, the CLR then needs to initialize its internal data structures, etc. On top of this our CIL code then needs, at runtime, to be JIT compiled to native code. It varies from case to case, but sometimes the initial cost of JIT compilation may just push you over your programs startup budget, in which case you would use AOT compilation so you forgo the costs of JIT compilation, and hopefully fall back within your startup budget. Another important thing to note about using AOT compilation is that you lose some of the advantages that JIT compilation provides.
Note: AOT compilation can be performed by using the ngen.exe tool in .NET, or for Mono you can specify the --aot flag as well as the optimizations you want to use when invoking the mono application launcher.
Instruction Sets
CIL consists of two instruction sets:
- Base instructions; and
- Object model instructions
The base instructions are somewhat analogous to native CPU instructions, and they provide a Turing-complete instruction set. There are 67 base instructions, examples of which include: call; break; add; switch; and shl.
The object model instructions provide an instruction set to help facilitate services which many high-level languages commonly make use of. There are 33 object model instructions, examples of which include: sizeof; ldobj; initiobj; throw; and ldstr.
We will revisit the two instruction sets in part three. For now the reader need only know that there exists two instruction sets within CIL.
Verifiable CIL
We will only touch on verification briefly, but it is important to know that before CIL is compiled by the JIT it is verified. For example, if you call a method and it requires as part of its formal parameters certain types, then the object reference, or value that you have on the stack that is the actual parameter is verified to make sure it is compliant with the expected types. Other properties, like each method having a return statement are also verified, as well as ensuring that you are not trampling on memory which you should not be touching. In contrast, you can choose to bypass such verification by marking a method or region of code as being unsafe. Code that is unsafe is trusted by the CLR, it is up to the programmer to make sure that the code within the unsafe region does not do anything untoward.
If you look at the source code that Mono and the SSCLI comprise of you will see direct calls to methods and macros alike that verify the CIL just before JIT compilation.
Summary
In this article we described the purpose of CIL, its role in both .NET and Mono, the various ways in which CIL is compiled, the two instruction sets that CIL comprises of, and finally we very briefly discussed CIL verification.
In the next part of the series we will disassemble a program and explore the makeup of a managed module.
About Granville Barnett
 |
Sorry, no bio is available
This author has published 32 articles on DotNetSlackers. View other articles or the complete profile here.
|
You might also be interested in the following related blog posts
MonoSpace Conference in Austin - October 27 through 30
read more
Visual Studio Add-In vs. Integration Package Part 1
read more
Telerik Announces Support for their ASP.NET controls on Mono!
read more
Weblogic JMS with .NET
read more
Get Your Func On
read more
Should you use type inference in C# (var) ?
read more
We Don't Need No Architects--Really!
read more
MonoDevelop 1.0 has been Released
read more
WCF & ASP.NET Role Provider
read more
The Languange conundrum; what should I choose?
read more
|
|
Please login to rate or to leave a comment.