Track down DLL loading using Visual Studio

Application performance matters --- including start up times, working sets and memory consumption.

One path leading to more efficient code is loading fewer DLLs. One should only be loading modules that are really needed. However, it is very easy to accidentally load more DLLs at startup than necessary leading to lower performance including longer startup times and higher memory consumption.

While there exists a variety of tools which tell you which processes are loaded (examples include tlist, process explore and visual studio -> modules window), one often needs to find out why a module was loaded to try to eliminate the DLL load to improve performance.

The callstack at the time the DLL was loaded helps in identifying why a particular dll was loaded, and therefore is essential for performance diagnostics.

This blog shows you how to do all this using Visual Studio! All the files you need to follow this example are provided in a ZIP file at the end of this post.

Consider the following (somewhat contrived) program. This program was intended to represent a program that could write is output either as XML or as simple text. Let us assume that the XML option as the rare case. Let us also assume that we care about the working set of this program. When we run it to completion under the Visual Studio debugger, we notice in the Debug -> Modules window that System.Xml.Linq.ni.dll is loaded even in the case when even in the common case where the user specified text output. This is clearly inefficient, and this investigation shows how we can track down the cause of this ‘questionable’ DLL load.

Program.cs

using System;

using System.IO;

using System.Xml.Linq;

class Program

{

    static void ProcessArgs(string[] args)

    {

      if (args.Length == 2 && args[0] == "text")

        {

            File.WriteAllText(args[1], "Here is the data I am writing");

        }

        else if (args.Length == 2 && args[0] == "xml")

        {

            XElement myXml = new XElement("data", "Here is the data I am writing");

            myXml.Save(args[1]);

        }

    }

    static int Main(string[] args)

    {

        ProcessArgs(args);

        return 0;

}

}

The overall structure of the investigation is to set a breakpoint at the OS method that loads the DLLs. When we are at the breakpoint we can determine which DLL is being loaded. When we find the DLL we are focusing on, we have a call stack, and this callstact is typically the key to understanding how our program cause the load (an thus how we might eliminate the load).

Step 1: Ensure access to the operating system symbols through the following commands in your VS command window

mkdir c:\symbols

set _NT_SYMBOL_PATH=SRV*C:\symbols*https://msdl.microsoft.com/download/symbols

Step 2: Launch the application, debugging it as an unmanaged application. This is needed because VS by default hides all activity in the runtime itself if you debug it as a managed application. By doing this, you get visibility into all DLL loads.

start devenv /debugexe <command line of application>

In our example, this translates to

start devenv /debugexe DemoDllLoad.exe text output.txt

(assuming Program.cs was compiled into the executable DemoDllLoad.exe)

Step 3: Right click on the EXE name in the solution explorer -> select Properties -> Set debugger type to ‘Native Only’

Step 4: Add a new breakpoint for the system call LoadLibraryExW@12. This OS function is called every time a DLL is loaded. To do this bring up the breakpoint dialog (Debug à New Breakpoint à Break at Function) and enter the following into the function field (ignore the other fields)

        {,,kernel32}_LoadLibraryExW@12                      

Select Apply and then click OK.

 

Step 5: Hit F5 (Go). This will now break every time a DLL is loaded, each time you will see a call stack in the call stack window where the DLL was loaded.

Step 6: The public symbols for the OS do not decode the arguments, but you can work around this by decoding the arguments ‘by hand’. After hitting the breakpoint for _LoadLibraryExW@12, go to the watch window (Ctrl-D W), and add the following expression to it

*(wchar_t**) (esp+4)

You should now see the string value of the DLL being loaded.

Step 7: Keep hitting F5 until the DLL you are interested in is in the watch window. In our example we keep going until System.Xml.Linq.ni.dll is loaded.

Step 8: Unfortunately, only the UNMANAGED part of the stack is displayed in the” Callstack window”. This may not be interesting (you care about the managed part of the stack). To see the managed part of the stack select the “Immediate window” and type the following (when System.Xml.Linq.ni.dll appears in the watch window):

.load sos                                                                             

(This only needs to be typed only once.)

!ClrStack                              

(You can use !Help for help)

Step 9: Observe the immediate window. In our example you will see something similar to the following:

PDB symbol for mscorwks.dll not loaded

OS Thread Id: 0x14ec (5356)

ESP EIP

0015ed4c 762430c3 [PrestubMethodFrame: 0015ed4c] Program.ProcessArgs(System.String[])

0015ed5c 007f008d Program.Main(System.String[])

0015ef7c 79e7c74b [GCFrame: 0015ef7c]

This tells us that the DLL (System.Xml.Linq.ni.dll) got loaded when Program.ProcessArgs() was called from Main() (and was being Jitted --- PresubMethodFrames are created for JIT compiling a method ).

Step 10: The reason why this DLL was getting loaded irrespective of the input is because the JIT compiler does not know what code paths will be taken, and has to load all DLLs referenced by that method when compiling it. This is because Program.ProcessArgs() had calls to methods in System.Xml.Linq.ni.dll that DLL had to be loaded (even if the methods are not ultimately called).

To prevent these unwanted DLL loads from happening, let us put the Xml processing in a separate routine as follows:

using System;

using System.IO;

using System.Xml.Linq;

class Program

{

    static void XmlProcessingRoutine(string filename)

    {

        XElement myXml = new XElement("data", "Here is the data I am writing");

        myXml.Save(filename);

    }

    static void ProcessArgs(string[] args)

    {

      if (args.Length == 2 && args[0] == "text")

        {

            File.WriteAllText(args[1], "Here is the data I am writing");

        }

        else if (args.Length == 2 && args[0] == "xml")

        {

       XmlProcessingRoutine(args[1]);

        }

    }

   

    static int Main(string[] args)

    {

        ProcessArgs(args);

        return 0;

    }

}

 

Now, System.Xml.Linq.ni.dll and System.Xml.ni.dll will only get loaded if the function XmlProcessingRoutine() is invoked, which will only happen when Xml output is selected in the command. Thus, for other cases, when text output is desired we have avoided loading two additional unwanted dlls! While in this case the program was probably simple enough that we could have guessed this without knowing the call stack that loaded the DLL, in real programs the call stack is usually the key to determining why a DLL is loaded.

The source files can be found as a zip file as an attachment here.

This post was authored by Vance Morrison (Principal Architect) and Subramanian Ramaswamy (Program Manager) from the CLR Perf team at Microsoft.

DemoDllLoad.zip

Comments