Friday, September 29, 2006 1:07 PM bart

.NET 2.0 string freezing explained - System.Runtime.CompilerServices.StringFreezingAttribute

Introduction

String freezing is a less known feature of .NET 2.0, not to be confused with string interning. The feature allows you to freeze a string in a GC segment at compile time (or better: at native image generation time, cf. ngen.exe) instead of having this done at runtime (the default behavior).

The good side: reduction of private pages, more efficiency.
The dark side: the native image of the assembly containing the frozen string cannot be unloaded (because of possible references to the frozen string that the runtime doesn't have control over).

As always, use performance optimizations with care and evaluate the applicability of this technique. If you decide to give it a go, measure the netto gains of applying it. The most suitable place to use this technique is when you have "string-rich" assemblies that shouldn't be unloaded because they app you're building can't live without them.

Play time

As usual a good starting point is the MSDN documentation for the thing: StringFreezingAttribute. The first thing to notice is that the attribute has to be applied on the assembly level. That is, all strings (known at compile time, cf. ldstr) in the assembly will end up in a pre-allocated GC segment spit out by the native image generator. That brings us to the second implication: the assembly has to be ngen-ed, which is the process of turning a managed code assembly in a native image well-suited for execution on the target processor. I've blogged about ngen in the past, so you might want to check out that post too.

So, let's create a simple application and see what it does. However, before you start, you'll have to compile my unmanaged AddressOf library which we'll use to display addresses of objects at runtime. Now create a console application (I've called it "DotNetPlayground") and reference the AddressInspector.dll file (or whatever it is called) containing the unmanaged AddressOf implementation. Time to add some code to Program.cs. Here you go:

using System;
using
System.Runtime.CompilerServices;

//[assembly: StringFreezing()]


namespace
DotNetPlayground
{
   class
Program
   {
      static void Main(string
[] args)
      {
         string s = "Hello world!"
;
         AddressInspector.AddressOf(s);
      }
  
}
}

That's right, we'll first compile without string freezing turned on. Now go to the Visual Studio 2005 command line, navigate to the folder of your app and execute it as few times:

...\DotNetPlayground\bin\Debug>DotNetPlayground.exe
014040D0

...\DotNetPlayground\bin\Debug>DotNetPlayground.exe
015940D0

Different addresses, right? Now ngen the assembly:

...\DotNetPlayground\bin\Debug>ngen DotNetPlayground.exe
Microsoft (R) CLR Native Image Generator - Version 2.0.50727.112
Copyright (C) Microsoft Corporation 1998-2002. All rights reserved.
Installing assembly ...\DotNetPlayground\bin\Debug\DotNetPlayground.exe
Compiling 1 assembly:
    Compiling assembly ...\DotNetPlayground\bin\Debug\DotNetPlayground.exe ...
DotNetPlayground, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null <debug>

and execute again:

...\DotNetPlayground\bin\Debug>DotNetPlayground.exe
014E1948

...\DotNetPlayground\bin\Debug>DotNetPlayground.exe
01671948

Random addresses again, and that's normal because the string isn't pre-allocated in a GC segment. That's what string freezing is responsible for, so let's turn it on:

using System;
using
System.Runtime.CompilerServices;

[assembly:
StringFreezing
()]

namespace
DotNetPlayground
{
   class
Program
   {
      static void Main(string
[] args)
      {
         string s = "Hello world!"
;
         AddressInspector
.AddressOf(s);
      }
   }
}

Before we carry on with compilation, switch back to the command line to unregister the ngen-ed image:

...\DotNetPlayground\bin\Debug>ngen uninstall DotNetPlayground.exe

Compile and execute:

...\DotNetPlayground\bin\Debug>DotNetPlayground.exe
015E1948

...\DotNetPlayground\bin\Debug>DotNetPlayground.exe
01701948

Just what we expected since we don't have created a native image yet. That's important to remember: without a native image, the StringFreezingAttribute doesn't have impact. It's there to serve ngen.exe in its mission of creating a native image exactly like you want it to be.

...\DotNetPlayground\bin\Debug>ngen DotNetPlayground.exe
Microsoft (R) CLR Native Image Generator - Version 2.0.50727.112
Copyright (C) Microsoft Corporation 1998-2002. All rights reserved.
Installing assembly ...\DotNetPlayground\bin\Debug\DotNetPlayground.exe
Compiling 1 assembly:
    Compiling assembly ...\DotNetPlayground\bin\Debug\DotNetPlayground.exe ...
DotNetPlayground, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null <debug>

Now execute and feel happy:

...\DotNetPlayground\bin\Debug>DotNetPlayground.exe
30004448

...\DotNetPlayground\bin\Debug>DotNetPlayground.exe
30004448

So, what has happened? The string "Hello World" ended up in the native image portion that "prepopulates" the GC segment. When the application (i.e. PE file) is loaded into memory, the string ends up at the same place every time (not talking about relocations etc). When the IL-equivalent to ldstr is executed, the fixed address is returned instead of performing an allocation at runtime on the GC heap.

Going deeper

So, what happened to our native image? Time for some inspection, so navigate to the NativeImages location on your machine (%windir%\assembly\NativeImages_v2.0.50727_32), cd into the DotNetPlayground folder that was created and then to a subfolder with some randomly looking name (dir for it):

C:\Windows\assembly\NativeImages_v2.0.50727_32\DotNetPlayground\4b5032f0418220bb
b61a84c7a39af83b>dir
 Volume in drive C is Windows Vista
 Volume Serial Number is C4FB-B1E7

 Directory of C:\Windows\assembly\NativeImages_v2.0.50727_32\DotNetPlayground\4b
5032f0418220bbb61a84c7a39af83b

22/09/2006  13:22    <DIR>          .
22/09/2006  13:22    <DIR>          ..
22/09/2006  13:22            19.456 DotNetPlayground.ni.exe
               1 File(s)         19.456 bytes
               2 Dir(s)  25.134.407.680 bytes free

This is where the native image (ni) lives. You can ildasm it, but there won't be much of IL left, just the metadata is there. A more interesting tool is the PE/COFF dumper dumpbin.exe that comes with the Platform SDK, the VS2005 tools and the new Windows SDK (so make sure it's on your search path by launching the right command prompt).

Inspection of the headers using dumpbin.exe /headers DotNetPlayground.ni.exe learns us:

Dump of file DotNetPlayground.ni.exe

...

OPTIONAL HEADER VALUES

                 ...
        30000000 image base (30000000 to 30013FFF)
                 ...

The address we saw before (30004448) falls in the image base as we expected. From the rest of the headers you'll find the raw section where the 30004448 address belongs to:

SECTION HEADER #2
   .data name
    2854 virtual size
    4000 virtual address (30004000 to 30006853)
         ...

When you now perform a dumpbin of the raw section data (e.g. using /all) you'll find the following:

  30004440: 00 00 00 00 EE 4E 00 8E E4 C6 0F 79 0D 00 00 00  ....¯N..õã.y....
  30004450: 0C 00 00 00 48 00 65 00 6C 00 6C 00 6F 00 20 00  ....H.e.l.l.o. .
  30004460: 77 00 6F 00 72 00 6C 00 64 00 21 00 00 00 00 00  w.o.r.l.d.!.....
  30004470: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................

"Hello world" pops out of misty clouds :-). CLR Profiler (Vista users, please read the installation note concerning ProfilerObj.dll registration) lovers can also use the EnumModuleFrozenObject method from ICorProfilerInfo2 to enumate frozen objects (and strings). Well sort of, the CLR Profiler tool doesn't have this support out of the box, but the Profiling API does. An example:

HRESULT hr = E_FAIL;

ICorProfilerObjectEnum* pEnum;
hr = m_pICorProfilerInfo2->EnumModuleFrozenObjects(moduleId, &pEnum);

if
(SUCCEEDED(hr))
{
   ULONG celt = 0;
   hr = pEnum->GetCount(&celt);

   if
(SUCCEEDED(hr) && celt > 0)
   {
      ObjectID* pFrozenObjectIds =
new
ObjectID[celt];

      while
(1)
      {
         hr = pEnum->Next(celt, pFrozenObjectIds, &celt);

         if
(SUCCEEDED(hr))
         {
            if
(0 == celt)
               break
;

            for
(ULONG i = 0; i < celt; ++i)
               LogEntry(
"Frozen object on address 0x%08X\r\n"
, pFrozenObjectIds[i]);
         }
      }

      delete
[] pFrozenObjectIds;
   }

   pEnum->Release();
}

Where the LogEntry function is a profiler callback function that ensures atomic printf and/or WriteFile logging. You can find the original code this is based on in the MSDN Magazine Article of January 2005 entitled "CLR Profiler: No Code Can Hide from the Profiling API in the .NET Framework 2.0" (see download).

The way profiling works is by turning it on by means of two environment variables:

  • COR_PROFILER is set to the profiler GUID (this is the GUID set in the C++ profiler code by assigning it to CLSID_PROFILER)

    #define PROFILER_GUID "{18884ADE-B15B-4af8-BE6C-FE5117BA4B32}"

    extern const GUID CLSID_PROFILER = { 0x18884ade, 0xb15b, 0x4af8, { 0xbe, 0x6c, 0xfe, 0x51, 0x17, 0xba, 0x4b, 0x32 } };

  • COR_ENABLE_PROFILING is set to 1 to turn on profiling

How to run this? Wel, grab the project from the article mentioned above and open up ProfilerCallback.cpp. Add the following #define:

#define SHOW_MODULE_LOADS 1

Now compile it. Go to a command prompt with administrative privileges and register profiler.dll (required on Vista):

...\CLRProfiler\bin\Debug>regsvr32 Profiler.dll

Now set two environment variables:

...\CLRProfiler\bin\Debug>set COR_ENABLE_PROFILING=1
...\CLRProfiler\bin\Debug>set COR_PROFILER={18884ADE-B15B-4af8-BE6C-FE5117BA4B32}

Now run our DotNetPlayground.exe file and be patient (it might take up to a few minutes). A file called output.log will appear in the current directory that can be used for profiling analysis. (Note: check the output of the application to ensure the native image was executed before checking the output.log file.)

Enjoy and don't get frozen by string freezing. Remember: shooting yourself in the foot is only a few seconds away in the world of computing.

Del.icio.us | Digg It | Technorati | Blinklist | Furl | reddit | DotNetKicks

Filed under:

Comments

# Attribut StringFreezing : gelez vos cha&amp;#238;nes litt&amp;#233;rales

Sunday, October 01, 2006 11:59 PM by CoqBlog

Encore un que je connaissais pas et que je viens de d&#233;couvrir via Bart De Smet&amp;nbsp;: l'attribut StringFreezing....