Share via


CLR Inside Out

The Performance Benefits of NGen.

Surupa Biswas

Contents

Why Use NGen?
Using NGen
Assemblies and the GAC
Choosing Base Addresses
Avoid Rebasing
NGen and Multiple AppDomains
Using Hardbinding
Regenerating NGen Images
Final Notes

T ypically, methods in managed executables are just-in-time (JIT) compiled. The machine code generated by the JIT compiler is thrown away once the process running that executable exits; therefore, the method must be recompiled when the application is run again. Moreover, the generated code is tied to the process that created it and cannot be shared between processes that are running the same application.

These characteristics of .NET JIT compilation can have performance drawbacks. Fortunately, NGen (native image generation) offers some relief. NGen refers to the process of precompiling Microsoft® intermediate language (MSIL) executables into machine code prior to execution time. This results in two primary performance benefits. First, it reduces application startup time by avoiding the need to compile code at run time. Second, it improves memory usage by allowing for code pages to be shared across multiple processes.

Although at first glance NGen appears to be similar to traditional static back-end compilation, it is actually quite different. Unlike statically compiled binaries, NGen images are tied to the machine where they are created and hence cannot be deployed. Instead, the application's installer needs to issue commands to create native images for the specific assemblies on the client machine at setup time. Also unlike traditional binaries, NGen images merely form a cache-managed applications will continue to run correctly even if all the NGen images are deleted. Of course, there will be a performance hit if that happens, but there will be no correctness issues. On the other hand, managed applications may fail to run correctly if the MSIL assemblies are deleted once they have been compiled into NGen images.

Why Use NGen?

NGen typically improves the warm startup time of applications, and sometimes the cold startup time as well. Cold startup time refers to the time taken to start an application soon after a machine reboot, such that the application is being launched for the first time after that reboot. Warm startup time, on the other hand, is the time taken to start the application given that it was launched in the recent past (and there were no reboots in between).

Cold startup time is primarily dominated by the number of pages that need to be fetched from disk. The improvement in cold startup time while using NGen can be attributed largely to the fact that pages of MSIL that need to be touched during compilation no longer need to be accessed at execution time.

Improvements in warm startup time come from reusing pages of the NGen images that were brought in when the application had been running earlier. This is especially beneficial to large client-side UI applications where startup time is critical to the user experience.

NGen also improves the overall memory usage of the system by allowing different processes that use the same assembly to share the corresponding NGen image among them, as shown in Figure 1. This can be very useful in both client and server scenarios in which the total memory footprint must be minimized. A classic example is the Terminal Services scenario in which a large number of users might be logged in and running the same application at the same time. If you're building libraries or other reusable components, you may also want to use NGen so that applications using your components can share the generated code pages.

Figure 1 Sharing Code Among Processes

Figure 1** Sharing Code Among Processes **

It is important to note that NGen does not always improve the cold startup time of applications, since NGen images are larger than MSIL assemblies. The only way to determine whether cold startup time and working set will improve or degrade with NGen for specific scenarios is to actually measure them. (More on performance measurement later.)

Using NGen

You can precompile your application with NGen by using the ngen.exe tool that ships with the Microsoft .NET Framework redistributable. The .NET Framework 2.0 has a completely reworked version of this tool and also has a new NGen service called the .NET Runtime Optimization service, which is capable of NGen-compiling assemblies in the background. Thus, NGen now supports both synchronous and asynchronous compilation. The complete syntax for ngen.exe can be found at .NET Framework Tools.

All NGen images are installed in the native image cache, which is located in the %WINDIR%\assembly directory. For example, "NGen install foo.exe" will create a native image called foo.ni.exe and install it in the native image cache. Figure 2 shows the location of the native image for System.Web.dll.

Figure 2 Native Image Cache

Figure 2** Native Image Cache **

Using the NGen tool you can choose to compile your assemblies either synchronously (for example, "NGen install foo.exe") or asynchronously with one of three priority levels (for example, "NGen install foo.exe /queue:3"). The asynchronous commands return immediately and are processed in the background by the NGen service. Assemblies queued up with priority 1 or 2 are compiled as soon as possible by the NGen service (priority 1 assemblies are compiled before priority 2 assemblies). Priority 3 assemblies are compiled by the service when it detects that the machine is idle. You can also use the executeQueuedItems command-line switch to force all queued items to be executed, as shown in Figure 3.

Figure 3 Asynchronous NGen Compilation

Figure 3** Asynchronous NGen Compilation **

Using synchronous NGen at install time guarantees that end users will never launch your application without all the assemblies having been precompiled by NGen. However, this causes setup to take longer because the compilation time is now added to the overall install time. As an alternative, the application's setup program can issue asynchronous NGen commands. Using asynchronous NGen with priority 1 or 2 ensures that the native images will be available soon after setup completes. There is, however, a risk of the compilation work adversely affecting the performance of other applications or the system's response time to user inputs.

The other option is to use asynchronous NGen with priority 3. Priority 3 compilation seems like it's "free" in the sense that it neither consumes machine cycles during setup nor disrupts system performance. However, since machines may not be idle for extended periods of time immediately after the install, the end user could launch and use the application before the NGen service gets an opportunity to compile the assemblies. Therefore you can finely tune this trade-off by dividing your application's assemblies into categories and precompiling each of these differently. Assemblies that are absolutely critical to startup time can be compiled synchronously, those that are important but not critical can be compiled by the service as priority 1 or 2, and those in less frequently used execution paths can be compiled by the service as priority 3.

Assemblies and the GAC

In order to get the optimal performance from NGen, all assemblies in the application need to be strong-named and installed into the Global Assembly Cache (GAC) prior to compilation. If an assembly is strong-named and not installed into the GAC, the loader will need to verify the assembly's signature at load time. Validating the strong-name signature requires the loader to parse the entire contents of the MSIL assembly, compute a hash, and then compare it to the assembly's signature. The verification process therefore fetches every page of the MSIL assembly from disk. This results in a spike in the application's working set, increases load time, and pollutes the OS disk cache.

If the assembly were installed in the GAC, however, the loader would not need to validate the strong-name signature at load time. This is because the signature of the assembly is verified at the time it is installed into the GAC. The loader assumes that since the GAC is a secure store that can only be modified by an administrator, and since the signature was verified at GAC installation time, it can skip revalidating the contents. When the loader attempts to load a pure MSIL assembly from the GAC and discovers a matching NGen image, it directly loads the NGen image. In that scenario, not even a single page of the MSIL assembly needs to be fetched into memory.

This difference in behavior usually causes a significant difference in the startup time of NGen-compiled applications. Unless you have a reason to not want to install the assemblies into the GAC, it is highly recommended that you do so in order to get the best performance from NGen.

Of course, the cost of validating the strong-name signature at run time in the non-GAC case is incurred regardless of whether the application is JIT-compiled or NGen-compiled. In the NGen case, it is just particularly painful since some or all of the MSIL pages that are brought into memory during signature verification may not be used during execution. Information on how to sign an assembly and then install it into the GAC can be found at "How to install an assembly into the Global Assembly Cache in Visual C#".

Choosing Base Addresses

The next important tip to remember is that in order to get good working set performance from NGen you must carefully choose base addresses for the application's assemblies such that the NGen images do not collide with each other. Note that if the application comprises only a single.exe and hence a single native image, choosing suitable base addresses, as described in this section, is not required.

When loading an executable file, managed or unmanaged, the operating system has to allocate a contiguous block of virtual memory whose size equals that of the executable. The contents of the file are then mapped to this memory block. Executable images can contain functions and methods, global variables and constants, and references to all of these. Many of these references are absolute memory addresses. They assume that the image will be loaded at a given address, which is called the preferred base address of the executable.

As long as the operating system manages to load the executable image at its preferred base address, all references contained in the image are valid and can be used right away. Furthermore, the image can be shared across multiple processes, which usually leads to substantial savings in the working set of shared components. However, if the loader cannot place the module at the desired address (because it overlaps with another module or piece of data already loaded or allocated), the module is rebased, meaning it is loaded at some other address. This implies that all addresses within the executable image need to be fixed up.

These fix ups are expensive from a throughput perspective, since they need to be applied at load time. They are also expensive from a working set perspective, since every page in the binary that contains a memory reference needs to be fetched into memory and updated, even though the reference might never actually be used during execution. Moreover, since all these pages have now been written to, they become private and can no longer be shared between processes. Thus, there is a substantial performance hit when native code is rebased.

In the case of managed applications precompiled with NGen, base address misses are even more expensive than they are for unmanaged binaries. This is because of hardbinding (more on hardbinding in a moment). If assembly A hardbinds to one of its dependencies B, and if B misses its preferred base address, not only do all pointers in B's NGen image need to be fixed up, but every pointer in A that refers to a type or a method in B needs to be fixed up, too. The fix ups required due to hardbinding can result in a large increase in the working set, to the extent that using NGen is no longer beneficial.

JIT-compiled code does not have a rebasing problem since the addresses are generated at run time based on where the code is placed in memory. Also, MSIL is rarely affected by base address misses since MSIL references are token-based, rather than address-based. Thus when the JIT compiler is used, the system is resilient to base address collisions.

Today there is no automatic support for base address management. When creating an assembly, if the developer does not explicitly specify a preferred base address, the compiler typically picks a default value. The unfortunate thing about letting the compiler assign base addresses is that all the binaries in the application end up with the same preferred load address.

Avoid Rebasing

The only way to avoid the rebasing scenario with NGen images that I've described is to carefully assign base addresses to the application's assemblies. When choosing base addresses, keep in mind that NGen images are significantly larger than MSIL assemblies, typically 2.5 to 3 times the size of their corresponding MSIL assemblies in the case of x86. They are even larger for 64-bit systems.

There is currently no way to directly specify the load address for an NGen image. What you can do is specify a base address for the MSIL assembly. For a pure MSIL assembly, NGen chooses the base address of the native image to be the same as that of the assembly. This works quite well, since at any point in time either the assembly or the NGen image must be loaded into memory, but not both. For a mixed-mode assembly (an assembly that contains both native and managed code), both the assembly and the NGen image might need to be loaded into memory together. Thus, for such assemblies NGen selects the base address of the NGen image to be equal to the base address of the MSIL assembly plus the size of the MSIL assembly plus some padding to round up to the nearest page boundary.

The 64-bit NGen images are larger than their corresponding 32-bit native images. Both of these can be generated from the same MSIL platform-neutral assembly. If NGen used the same base addresses for both 32-bit and 64-bit native images, selection of appropriate base addresses for 32-bit systems would lead to relocations on 64-bit systems; on the other hand, selection of appropriate base addresses for 64-bit systems would lead to inefficient address space usage on 32-bit systems. NGen solves this problem by choosing base addresses for the 64-bit images based on the formula (Offset + N * Base address of MSIL assembly). For the .NET Framework 2.0, Offset is set to 0x64180000000 and N is set to 2 on x64 systems (these values might change in future releases of the .NET Framework). Correct selection of base addresses for 32-bit images typically should just work for 64-bit images, thanks to this algorithm.

The challenge for the developer then is to choose the appropriate addresses for the MSIL assemblies. Since these native images are approximately three times the size of the corresponding assemblies, one method is to make sure that two adjacent assemblies A1 and A2 are separated by at least three times the size of A1. In the case of mixed-mode assemblies, since both the MSIL and the NGen image will need to be loaded together, A1 and A2 will need to be separated by at least four times the size of A1. It is generally a good idea to leave some additional space, since assemblies (and hence the NGen images) might grow with time because of servicing or localization. Also, the factor of three or four is merely a heuristic, although it works for most assemblies. The only way to figure out the exact multiplier for a particular assembly would be to actually NGen-compile the assembly and compare the size of the image generated to that of the MSIL assembly. Since NGen itself multiplies the base address of the MSIL by N, choosing addresses that ensure that the 32-bit NGen images do not overlap with each other will typically ensure that 64-bit native images do not either.

Base addresses can be assigned to assemblies from within Visual Studio® 2005 (as shown in Figure 4) or by using the /baseaddress switch in the C# compiler (see "How to: Specify a Base Address for a DLL" and "/baseaddress (Specify Base Address of DLL) (C# Compiler Options)" for more information). Once base addresses have been assigned to each assembly, it is important to install the assemblies in the GAC, NGen-compile them, and then run the application to make sure that there are indeed no collisions. A tool that can be used to view base address collisions is Process Explorer from Sysinternals. This tool displays the preferred and actual addresses of all binaries that get loaded into memory and highlights the ones that get relocated.

Figure 4 Setting a Base Address in Visual Studio 2005

Figure 4** Setting a Base Address in Visual Studio 2005 **

NGen and Multiple AppDomains

When using multiple AppDomains, assemblies are loaded by default into AppDomains as domain-specific. This implies that each assembly can only be used in the context of the particular AppDomain that it was loaded into. If two AppDomains load the same assembly as domain-specific, both get their own copy of the JIT-compiled code. Alternatively, you can specify assemblies to be loaded as domain-neutral. When that happens, the assemblies are loaded into a special AppDomain called the shared domain. An assembly in the shared domain does not belong to the AppDomain which attempted to load it; instead, it belongs to all AppDomains in the process. Such assemblies are not unloaded until the process exits. Thus, another AppDomain that needs to use the same domain-neutral assembly can simply use the copy already loaded into the shared domain.

In order to get good performance when using NGen on applications with multiple AppDomains, all assemblies should be loaded as domain-neutral. When an assembly with an NGen image is loaded into an AppDomain as domain-specific, the NGen image can only be used the first time the assembly is loaded. Subsequent attempts to load the same assembly into different AppDomains will result in code being JIT-compiled. This happens because when the NGen image is loaded as domain-specific, the loader initializes data structures in the NGen image resulting in the image being hardwired to that AppDomain. Once that happens, the code essentially becomes nonshareable.

An alternative approach to falling back to JIT compilation is to load different copies of the native image into every AppDomain that loads the corresponding assembly as domain-specific. However, this approach would require every copy of the NGen image, except for the first, to be loaded at a different address from its preferred base address. Thus all of these copies of the NGen image would need their addresses fixed up.

Loading an assembly with generic instantiations in it as domain-specific can result in JIT compilation even when the code for the instantiation is stored in the NGen image and even though this might be the first time the assembly is being loaded into an AppDomain. This happens because the system requires that domain-neutral generic types be instantiated as domain-neutral so that the instantiation can be used across application domains. Consider an example where assembly A contains a generic instantiation called GI that uses value types T1 and T2. If both T1 and T2 are defined in assemblies that are loaded as domain-neutral, GI is treated as a domain-neutral generic instantiation. Now if an AppDomain attempts to load assembly A as domain-specific, the system needs to JIT-compile this instantiation, even though the code for it was available in A's NGen image.

As I mentioned earlier, JIT-compiling code in the scenarios I just described can be avoided by loading the assemblies as domain-neutral. There are some trade-offs involved here, however. Domain neutrality does not come for free. First, a domain-neutral assembly cannot be unloaded even when all the AppDomains that were using it have been unloaded. Second, NGen-compiled code is slower than JIT-compiled code when it comes to accessing static variables. This is because each AppDomain has its own copy of the static variable stored at a different location in memory. Since the same NGen-compiled code is executed by every AppDomain, the code first needs to detect the AppDomain whose context it is running in and then load or store the corresponding static variable. This extra code may result in some decrease in throughput performance in the case of C++-style code that makes heavy use of statics. However, the effect on real application code (as opposed to ported old-style benchmarks) is typically minimal. You should measure the performance in the domain-specific and domain-neutral cases and check whether there is any degradation.

An assembly can be loaded as domain-neutral by setting the LoaderOptimizationAttribute on the assembly's main method to either MultiDomain or MultiDomainHost. The default behavior of the system is to only share mscorlib.dll across all AppDomains. Setting this attribute to MultiDomain in assembly A will enable A to be shared; setting it to MultiDomainHost will enable A to be shared, but only when A is loaded from the GAC. For the latter case, assemblies not loaded from the GAC will not be shared, and hence they can be unloaded at any time. For the performance reasons I outlined previously, most assemblies are likely to be installed in the GAC, and so there should be little difference between MultiDomain and MultiDomainHost.

Using Hardbinding

Throughput performance of NGen-compiled code is worse than that of JIT-compiled code. This is one of the most common reasons cited in the literature about why you might not want to use NGen. The throughput of NGen-compiled code was on an average 5-10 percent worse than that of its JIT-compiled counterpart in versions 1.0 and 1.1 of the .NET Framework. In the .NET Framework 2.0, improvements have been made to NGen to narrow the gap in throughput performance. The improvement is due to a new feature called hardbinding. In the .NET Framework 2.0, all NGen images, by default, hardbind to the NGen images of mscorlib.dll and system.dll. This change by itself has made the throughput of NGen-compiled code comparable to that of JIT-compiled code. You may also choose to hardbind some of your assemblies to certain dependencies to further improve performance.

Throughput of NGen-compiled code is lower than that of JIT-compiled code primarily for one reason: cross-assembly references. In JIT-compiled code, cross-assembly references can be implemented as direct calls or jumps since the exact addresses of these references are known at run time. For statically compiled code, however, cross-assembly references need to go through a jump slot that gets populated with the correct address at run time by executing a method pre-stub. The method pre-stub ensures, among other things, that the native images for assemblies referenced by that method are loaded into memory before the method is executed. The pre-stub only needs to be executed the first time the method is called; it is short-circuited out for subsequent calls. However, every time the method is called, cross-assembly references do need to go through a level of indirection. This is principally what accounted for the 5-10 percent drop in throughput for NGen-compiled code when compared to JIT-compiled code.

In the .NET Framework 2.0, a new concept has been introduced-that of hardbound and softbound dependencies. Softbound dependencies are the regular kind of dependencies for which the behavior is the same as what I just outlined. Hardbound dependencies are those that are "high confidence" dependencies in the sense that these are either always or almost always loaded. If assembly A hardbinds to one of its dependencies B, code in A's NGen image will directly refer to types and methods in B's NGen image by assuming that B's image will get loaded at its preferred base address. Thus hardbinding has the advantage of avoiding the extra level of indirection that is typically required in NGen-compiled code while making cross-assembly references. This can substantially improve throughput performance. In most cases, the working set improves, too, since the fix up table pages are no longer required.

Hardbinding, however, has a few disadvantages. First, all hardbound dependencies of an assembly need to be loaded into memory before the assembly itself can be loaded. Many of these assemblies might not be required until much later (or they may not be used at all), and it can be more economic from a working set perspective to have these loaded lazily. Second, if a hardbound dependency gets relocated, NGen images for both assemblies need to be fixed up. Thus, if there are a lot of assemblies that hardbind to a particular dependency, and if that assembly happens to get rebased, the cost of performing the fix ups typically nullifies the working set savings from having used NGen.

Because of this second reason, you need to carefully choose the preferred load address of an assembly before deciding to let other assemblies hardbind to it. For instance, the base address of mscorlib.dll has been carefully chosen to minimize the chances of it being rebased. Hence for the majority of the scenarios, hardbinding all assemblies to mscorlib.dll improves throughput but does not increase the working set size. To further improve throughput, you can set attributes on your assemblies in order to hardbind them to their .NET Framework or non-.NET Framework dependencies. For example, assembly A can be hardbound to its dependency by either setting the DependencyAttribute on A or the DefaultDependencyAttribute on B. Thus if assembly A frequently calls assembly B, the developer can set the DependencyAttribute in the code for A such that it hardbinds to B:

[assembly: DependencyAttribute("AssemblyB", LoadHint.Always)]

On the other hand, if all the assemblies in the application make a lot of calls to assembly B, under all the interesting scenarios, the developer can instead just set the DefaultDependencyAttribute in the code for B such that all assemblies hardbind to B:

[assembly: DefaultDependencyAttribute(LoadHint.Always)]

Regenerating NGen Images

What I'm about to say is neither about getting good memory utilization from NGen, nor about getting good throughput. It is about making sure that the NGen images are available at all times. In order to make sure that NGen images are always available, they need to be regenerated when they become invalid.

There are a number of reasons why NGen images might get invalidated after they have been created. For example, the native image of assembly A gets invalidated by updates made to A, updates made to any of A's dependencies, changes to security policy between NGen time and run time (such as changing the trust policy from full trust to partial trust), changes to binding decisions (such as adding an application configuration file with binding redirects), and so on. These invalidations are due to the way NGen works today. For instance, native images contain fix ups for cross-assembly references (described earlier) and these are expressed in terms of metadata tokens. Once assembly A is recompiled, its metadata tokens change, and all native images in the system that had used assembly A's tokens earlier are now rendered invalid. For hardbound dependencies, there are no fix ups, and instead direct addresses are used. These direct addresses, of course, are also rendered invalid when the dependency is recompiled.

Changing the security policy occurs because while generating native images, NGen uses the security policy effective at that time to resolve link and inheritance demands. If the security policy is changed at a later time, these demands need to be reevaluated and hence the code in the NGen image is no longer valid.

The most common reason for an NGen image to become invalid is that its corresponding assembly or one of its dependencies has been serviced. Therefore, whenever an assembly is patched, the NGen images for all assemblies that depend on it and the NGen image for the assembly itself (in case it exists) must be regenerated.

NGen images can be regenerated by using the update command in ngen.exe. Issuing this command computes the list of all native images on the machine that need to be recompiled, compiles them, installs them into the native image cache, and then deletes the invalidated images. Thus, every patch to the .NET Framework 2.0 will issue the update command at the end to ensure that all native images in the system, including those generated from third-party assemblies, are kept up to date. In the future, the update command will also be issued whenever a new version of the .NET Framework is installed on the machine or whenever an existing version is uninstalled. Thus, NGen images will be available even though the install/uninstall may result in applications being rolled back or floated forward to a different version of the Framework. In that sense, the ngen.exe tool is compatible with multiple versions. For example, running version N of the ngen.exe tool on an application that is configured to run against version N-1 of the .NET Framework will actually generate version N-1 native images.

You should also remember to regenerate the NGen images whenever you issue patches to your applications. The update command has a very simple syntax-"NGen update" or "NGen update /queue"-for synchronous and asynchronous NGen, respectively. In fact, if the end user runs into issues with NGen images, most of the time these can be resolved by simply typing "NGen update" on the command line. Typically, however, end users should not run ngen.exe on random managed assemblies installed on their machines, unless they have a specific reason for doing so. Adding new native images to the native image cache results in extra disk space usage and extra machine cycles spent recompiling the images after servicing events.

Final Notes

Before I wrap up this discussion of NGen, it is important to mention that creating NGen images for all assemblies in an application and the transitive closure of its dependencies does not guarantee that the JIT compiler (mscorjit.dll) will not be loaded at run time. When mscorjit.dll is loaded into memory, it adds another 200KB or so to the overall memory footprint on an x86 system (the JIT compiler is shared across all managed applications). More importantly, whenever the JIT compiler is loaded, it implies that code is being compiled on the fly, which in turn implies that MSIL pages are being accessed, private code pages are being added to the application's working set, and so forth. It is therefore a good idea to check to see whether mscorjit.dll actually gets loaded into your process when you run your application after having created the NGen images. You can do that by simply attaching a debugger or by using the Assembly Binding Log Viewer.

The latter, shown in Figure 5, enables you to log all binds to disk and also view the list of NGen images being loaded (each log entry details the loading and verification process, as shown in Figure 6). If the list of native images is empty or if certain dependencies are missing, MSIL was being JIT-compiled. There is also a new JitCompilationStart Managed Debugging Assistant that can be used to detect whether JIT-compilation happens when the application is executed. For more information on Managed Debugging Assistants, see Stephen Toub's article in this issue.

Figure 6 Log for the SampleApp1.exe Native Image

*** Assembly Binder Log Entry (2/16/2006 @ 1:23:28 PM)** * The operation was successful. Bind result: hr = 0x0. The operation completed successfully. Assembly manager loaded from: C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\mscorwks.dll Running under executable C:\SampleApp1\SampleApp1.exe --- A detailed error log follows. LOG: Start binding of native image SampleApp1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=736435799a064253. LOG: IL assembly loaded from C:\SampleApp1\SampleApp1.exe. LOG: Start validating native image SampleApp1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=736435799a064253. LOG: Start validating all the dependencies. LOG: [Level 1]Start validating native image dependency System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089. LOG: [Level 2]Start validating native image dependency mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089. LOG: [Level 2]Start validating IL dependency System.Xml, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089. LOG: [Level 2]Start validating IL dependency System.Configuration, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a. LOG: Dependency evaluation succeeded. LOG: [Level 1]Start validating native image dependency mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089. LOG: Dependency evaluation succeeded. LOG: [Level 1]Start validating IL dependency SampleLibrary1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=a2b6b55ab9a3daa5. LOG: Dependency evaluation succeeded. LOG: [Level 1]Start validating IL dependency System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a. LOG: Dependency evaluation succeeded. LOG: [Level 1]Start validating IL dependency System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089. LOG: Dependency evaluation succeeded. LOG: Validation of dependencies succeeded. LOG: Start loading all the dependencies into load context. LOG: Loading of dependencies succeeded. LOG: Bind to native image succeeded. Native image has correct version information. Attempting to use native image C:\WINDOWS\assembly\NativeImages_v2.0.50727_32\SampleApp1\ 05113aeb2745b64999e6b9959b8bd844\SampleApp1.ni.exe. Native image successfully used.

Figure 5 Log Viewer

Figure 5** Log Viewer **

When JIT-compilation is detected, you should first check to see whether there were dependencies that could not be NGen-compiled because they could not be found. A good way to check whether there were any missing dependencies is to look at the output from ngen.exe and see whether there were any errors or warnings generated. However, even when there are no errors or warnings, the JIT might still be invoked due to use of certain APIs. For example, APIs that create MSIL or source code on the fly, such as Reflection.Emit, System.Text.RegularExpressions.RegexOptions.Compiled, System.CodeDom, System.Xml.Serialization, Type.MakeGenericType, MethodInfo.MakeGenericMethod will cause JIT-compilation. Also, the use of certain loader APIs, such as Assembly.LoadFrom, require JIT-compilation, since Assembly.LoadFrom may return a different assembly each time it is called.

When considering the use of NGen, measure your application's working set size with and without having used NGen. To get an accurate reflection of the memory improvements from NGen, first build the application's setup program that installs the appropriate assemblies into the GAC and the native image cache. Then compare this to the configuration where the assemblies are added to the GAC, but not to the native image cache. Simply NGen-compiling assemblies and looking at the working set with and without having an NGen image for a particular assembly may not provide an accurate reflection.

Finally, it is good to weigh the benefits against the costs before committing to the use of NGen. The benefits include better sharing, faster startup time, and smaller memory footprint. The costs include extra disk space requirements and servicing overhead to keep the images up to date.

Send your questions and comments to  clrinout@microsoft.com.

Surupa Biswas is a program manager on the CLR team at Microsoft. She works on the runtime's back-end compiler and focuses primarily on pre-compilation technologies.