Home Random Page


CATEGORIES:

BiologyChemistryConstructionCultureEcologyEconomyElectronicsFinanceGeographyHistoryInformaticsLawMathematicsMechanicsMedicineOtherPedagogyPhilosophyPhysicsPolicyPsychologySociologySportTourism






Nbsp;   Executing Your Assembly’s Code

As mentioned earlier, managed assemblies contain both metadata and IL. IL is a CPU-independent machine language created by Microsoft after consultation with several external commercial and academic language/compiler writers. IL is a much higher-level language than most CPU machine languages. IL can access and manipulate object types and has instructions to create and initialize ob- jects, call virtual methods on objects, and manipulate array elements directly. It even has instructions to throw and catch exceptions for error handling. You can think of IL as an object-oriented machine language.

Usually, developers will program in a high-level language, such as C#, Visual Basic, or F#. The com- pilers for these high-level languages produce IL. However, as any other machine language, IL can be written in assembly language, and Microsoft does provide an IL Assembler, ILAsm.exe. Microsoft also provides an IL Disassembler, ILDasm.exe.

Keep in mind that any high-level language will most likely expose only a subset of the facilities offered by the CLR. However, the IL assembly language allows a developer to access all of the CLR’s facilities. So, should your programming language of choice hide a facility the CLR offers that you re- ally want to take advantage of, you can choose to write that portion of your code in IL assembly or perhaps another programming language that exposes the CLR feature you seek.

The only way for you to know what facilities the CLR offers is to read documentation specific to the CLR itself. In this book, I try to concentrate on CLR features and how they are exposed or not exposed by the C# language. I suspect that most other books and articles will present the CLR via a language perspective, and that most developers will come to believe that the CLR offers only what the develop- er’s chosen language exposes. As long as your language allows you to accomplish what you’re trying to get done, this blurred perspective isn’t a bad thing.

       
   
 
 

 

To execute a method, its IL must first be converted to native CPU instructions. This is the job of the

CLR’s JIT ( just-in-time) compiler.

 

Figure 1-4 shows what happens the first time a method is called.

 

Just before the Main method executes, the CLR detects all of the types that are referenced by

Main’s code. This causes the CLR to allocate an internal data structure that is used to manage access


to the referenced types. In Figure 1-4, the Main method refers to a single type, Console, causing the CLR to allocate a single internal structure. This internal data structure contains an entry for each method defined by the Console type. Each entry holds the address where the method’s implemen- tation can be found. When initializing this structure, the CLR sets each entry to an internal, undocu- mented function contained inside the CLR itself. I call this function JITCompiler.

When Main makes its first call to WriteLine, the JITCompiler function is called. The JIT­ Compiler function is responsible for compiling a method’s IL code into native CPU instructions. Because the IL is being compiled “just in time,” this component of the CLR is frequently referred to as a JITter or a JIT compiler.



       
   
 
 

 

FIGURE 1-4Calling a method for the first time.


When called, the JITCompiler function knows what method is being called and what type de- fines this method. The JITCompiler function then searches the defining assembly’s metadata for the called method’s IL. JITCompiler next verifies and compiles the IL code into native CPU instructions. The native CPU instructions are saved in a dynamically allocated block of memory. Then, JITCom­ piler goes back to the entry for the called method in the type’s internal data structure created by the CLR and replaces the reference that called it in the first place with the address of the block of memory containing the native CPU instructions it just compiled. Finally, the JITCompiler function jumps to the code in the memory block. This code is the implementation of the WriteLine method (the version that takes a String parameter). When this code returns, it returns to the code in Main, which continues execution as normal.

Main now calls WriteLine a second time. This time, the code for WriteLine has already been verified and compiled. So the call goes directly to the block of memory, skipping the JITCompiler function entirely. After the WriteLine method executes, it returns to Main. Figure 1-5 shows what the process looks like when WriteLine is called the second time.

 
 

 

FIGURE 1-5Calling a method for the second time.


A performance hit is incurred only the first time a method is called. All subsequent calls to the method execute at the full speed of the native code because verification and compilation to native code don’t need to be performed again.

The JIT compiler stores the native CPU instructions in dynamic memory. This means that the com- piled code is discarded when the application terminates. So if you run the application again in the future or if you run two instances of the application simultaneously (in two different operating system processes), the JIT compiler will have to compile the IL to native instructions again. Depending on the application, this can increase memory consumption significantly compared to a native application whose read-only code pages can be shared by all instances of the running application.

For most applications, the performance hit incurred by JIT compiling isn’t significant. Most applica- tions tend to call the same methods over and over again. These methods will take the performance hit only once while the application executes. It’s also likely that more time is spent inside the method than calling the method.

You should also be aware that the CLR’s JIT compiler optimizes the native code just as the back end of an unmanaged C++ compiler does. Again, it may take more time to produce the optimized code, but the code will execute with much better performance than if it hadn’t been optimized.

There are two C# compiler switches that impact code optimization: /optimize and /debug. The following table shows the impact these switches have on the quality of the IL code generated by the C# compiler and the quality of the native code generated by the JIT compiler.

 

Compiler Switch Settings C# IL Code Quality JIT Native Code Quality
/optimize­ /debug­ (this is the default) Unoptimized Optimized
/optimize­ /debug(+/full/pdbonly) Unoptimized Unoptimized
/optimize+ /debug(­/+/full/pdbonly) Optimized Optimized

 

With /optimize­, the unoptimized IL code produced by the C# compiler contains many no- operation (NOP) instructions and also branches that jump to the next line of code. These instructions are emitted to enable the edit-and-continue feature of Visual Studio while debugging and the extra instructions also make code easier to debug by allowing breakpoints to be set on control flow instruc- tions such as for, while, do, if, else, try, catch, and finally statement blocks. When produc- ing optimized IL code, the C# compiler will remove these extraneous NOP and branch instructions, making the code harder to single-step through in a debugger because control flow will be optimized. Also, some function evaluations may not work when performed inside the debugger. However, the

IL code is smaller, making the resulting EXE/DLL file smaller, and the IL tends to be easier to read for

those of you (like me) who enjoy examining the IL to understand what the compiler is producing.


Furthermore, the compiler produces a Program Database (PDB) file only if you specify the

/debug(+/full/pdbonly) switch. The PDB file helps the debugger find local variables and map the IL instructions to source code. The /debug:full switch tells the JIT compiler that you intend to debug the assembly, and the JIT compiler will track what native code came from each IL instruction. This allows you to use the just-in-time debugger feature of Visual Studio to connect a debugger to an already-running process and debug the code easily. Without the /debug:full switch, the JIT compiler does not, by default, track the IL to native code information, which makes the JIT compiler run a little faster and also uses a little less memory. If you start a process with the Visual Studio de-

bugger, it forces the JIT compiler to track the IL to native code information (regardless of the /debug switch) unless you turn off the Suppress JIT Optimization On Module Load (Managed Only) option in Visual Studio.

When you create a new C# project in Visual Studio, the Debug configuration of the project has /optimize­ and /debug:full switches, and the Release configuration has /optimize+ and /debug:pdbonly switches specified.

For those developers coming from an unmanaged C or C++ background, you’re probably thinking about the performance ramifications of all this. After all, unmanaged code is compiled for a specific CPU platform, and, when invoked, the code can simply execute. In this managed environment, com- piling the code is accomplished in two phases. First, the compiler passes over the source code, doing as much work as possible in producing IL. But to execute the code, the IL itself must be compiled

into native CPU instructions at run time, requiring more non-shareable memory to be allocated and requiring additional CPU time to do the work.

Believe me, because I approached the CLR from a C/C++ background myself, I was quite skeptical and concerned about this additional overhead. The truth is that this second compilation stage that occurs at run time does hurt performance, and it does allocate dynamic memory. However, Microsoft has done a lot of performance work to keep this additional overhead to a minimum.

If you too are skeptical, you should certainly build some applications and test the performance for yourself. In addition, you should run some nontrivial managed applications Microsoft or others have produced, and measure their performance. I think you’ll be surprised at how good the performance actually is.

You’ll probably find this hard to believe, but many people (including me) think that managed ap- plications could actually outperform unmanaged applications. There are many reasons to believe this. For example, when the JIT compiler compiles the IL code into native code at run time, the compiler knows more about the execution environment than an unmanaged compiler would know. Here are some ways that managed code can outperform unmanaged code:

■ A JIT compiler can determine if the application is running on an Intel Pentium 4 CPU and pro- duce native code that takes advantage of any special instructions offered by the Pentium 4. Usually, unmanaged applications are compiled for the lowest-common-denominator CPU and avoid using special instructions that would give the application a performance boost.


■ A JIT compiler can determine when a certain test is always false on the machine that it is run- ning on. For example, consider a method that contains the following code.

 

if (numberOfCPUs > 1) {

...

}

 

This code could cause the JIT compiler to not generate any CPU instructions if the host machine has only one CPU. In this case, the native code would be fine-tuned for the host machine; the resulting code is smaller and executes faster.

■ The CLR could profile the code’s execution and recompile the IL into native code while the ap- plication runs. The recompiled code could be reorganized to reduce incorrect branch predic- tions depending on the observed execution patterns. Current versions of the CLR do not do this, but future versions might.

These are only a few of the reasons why you should expect future managed code to execute better than today’s unmanaged code. As I said, the performance is currently quite good for most applica- tions, and it promises to improve as time goes on.

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 run time, 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 run time. 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 by 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 concur- rently, and during application initialization instead of compiling the methods just in time as the user is interacting with your application.

 


Date: 2016-03-03; view: 669


<== previous page | next page ==>
Nbsp;   Loading the Common Language Runtime | IL and Verification
doclecture.net - lectures - 2014-2024 year. Copyright infringement or personal data (0.008 sec.)