If your experiments show that the CLR’s JIT compiler doesn’t offer your application the kind of
performance it requires, you may want to take advantage of the NGen.exe tool that ships with the .NET
Framework SDK. This tool compiles all of an assembly’s IL code into native code and saves the resulting
native code to a file on disk. At runtime, when an assembly is loaded, the CLR automatically checks to
see whether a precompiled version of the assembly also exists, and if it does, the CLR loads the
precompiled code so that no compilation is required at runtime. Note that NGen.exe must be
conservative about the assumptions it makes regarding the actual execution environment, and for this
reason, the code produced by NGen.exe will not be as highly optimized as the JIT compiler–produced
code. I’ll discuss NGen.exe in more detail later in this chapter.
In addition, you may want to consider using the
System.Runtime.ProfileOptimization class.
This class causes the CLR to record (to a file) what methods get JIT compiled while your application is
running. Then, on a future startup of your application, the JIT compiler will concurrently compile these
methods using other threads if your application is running on a machine with multiple CPUs. The end
result is that your application runs faster because multiple methods get compiled concurrently, and
during application initialization instead of compiling the methods just-in-time as the user is interacting
with your application.
IL and Verification
IL is stack-based, which means that all of its instructions push operands onto an execution stack and
pop results off the stack. Because IL offers no instructions to manipulate registers, it is easy for people
to create new languages and compilers that produce code targeting the CLR.
IL instructions are also typeless. For example, IL offers an
add instruction that adds the last two
operands pushed on the stack. There are no separate 32-bit and 64-bit versions of the
add instruction.
When the
add instruction executes, it determines the types of the operands on the stack and performs
the appropriate operation.
In my opinion, the biggest benefit of IL isn’t that it abstracts away the underlying CPU. The biggest
benefit IL provides is application robustness and security. While compiling IL into native CPU
instructions, the CLR performs a process called verification. Verification examines the high-level IL code
and ensures that everything the code does is safe. For example, verification checks that every method
is called with the correct number of parameters, that each parameter passed to every method is of the
correct type, that every method’s return value is used properly, that every method has a return
statement, and so on. The managed module’s metadata includes all of the method and type
information used by the verification process.
In Windows, each process has its own virtual address space. Separate address spaces are necessary
because you can’t trust an application’s code. It is entirely possible (and unfortunately, all too common)
that an application will read from or write to an invalid memory address. By placing each Windows
process in a separate address space, you gain robustness and stability; one process can’t adversely
affect another process.