デバッグ技 : .ini ファイルによる JIT コンパイラ制御

NyaRuRuさんが書いていた 64bit CLR の話とほとんどかぶっているネタですが、JIT コンパイラを制御するデバッグ技が紹介されていたので勝手に抜粋翻訳。

via Scott Hanselman's ComputerZen.com
Release IS NOT Debug: 64bit Optimizations and C# Method Inlining in Release Build Call Stacks

関連ネタ

話の発端は、リリースビルドとデバッグビルドで例外時のスタックトレースの結果が違うこと。

using System;

class NormalProgram
{
  static void Main(string[] args)
  {
    try
    {
      methodA();
    }
    catch (System.Exception e)
    {
      Console.WriteLine(e.ToString());
    }
  }
  static void methodA() { methodB(); }
  static void methodB() { methodC(); }
  static void methodC() { badMethod(); }
  static void badMethod() { throw new ApplicationException("generic bad thing"); }
}

デバッグビルドとリリースビルドでこれを作って実行してみます。まずはデバッグビルドの 32bit版。

System.ApplicationException: generic bad thing
   場所 NormalProgram.badMethod() 場所 ...\Program.cs:行 19
   場所 NormalProgram.methodC() 場所 ...\Program.cs:行 18
   場所 NormalProgram.methodB() 場所 ...\Program.cs:行 17
   場所 NormalProgram.methodA() 場所 ...\Program.cs:行 16
   場所 NormalProgram.Main(String[] args) 場所 ...\Program.cs:行 9

きれいにトレースが取れています。ところで、英語版だと最初の「場所」が at で、真ん中の「場所」が in なんですねw 翻訳してくれないほうがよかったようなw

一方、リリースビルドの 32bit では、

System.ApplicationException: generic bad thing
   場所 NormalProgram.badMethod() 場所 ...\Program.cs:行 19
   場所 NormalProgram.Main(String[] args) 場所 ...\Program.cs:行 9

こうなってしまう。最適化による影響です。

次にインライン化を抑制してみます。

using System;
using System.Runtime.CompilerServices;

class NormalProgram
{
  [MethodImpl( MethodImplOptions.NoInlining )]
  static void Main( string[] args )
  {
    try
    {
      methodA();
    }
    catch ( System.Exception e )
    {
      Console.WriteLine( e.ToString() );
    }
  }
  [MethodImpl( MethodImplOptions.NoInlining )]
  static void methodA() { methodB(); }
  [MethodImpl( MethodImplOptions.NoInlining )]
  static void methodB() { methodC(); }
  [MethodImpl( MethodImplOptions.NoInlining )]
  static void methodC() { badMethod(); }
  static void badMethod() { throw new ApplicationException( "generic bad thing" ); }
}

32bit リリースビルドを実行すると

System.ApplicationException: generic bad thing
   場所 NormalProgram.badMethod() 場所 ...\Program.cs:行 24
   場所 NormalProgram.methodC() 場所 ...\Program.cs:行 23
   場所 NormalProgram.methodB() 場所 ...\Program.cs:行 21
   場所 NormalProgram.methodA() 場所 ...\Program.cs:行 19
   場所 NormalProgram.Main(String[] args) 場所 ...\Program.cs:行 11

インライン化が抑止でき、スタックトレースが完全に取れました。今度は 64bit リリースで実行します。x64 環境がないので(T^T)ここは試してません。

System.ApplicationException: generic bad thing
   at NormalProgram.methodC() in NormalProgram.cs:line 23
   at NormalProgram.Main(String[] args) in NormalProgram.cs:line 11

インライン抑止属性は無視されてしまいました。64bit CLR は積極的にインライン化します。インライン化の例というより、末尾最適化の例です。


じゃあ、完全なスタックトレースを取るにはどうしたらよいか?悪い例を3つ挙げてますが省略。

良い方法は .ini ファイルによる [.NET Framework Debugging Control] セクションを使うことです。この JIT 構成には、2つの面があります。

  • JIT コンパイラに追跡情報を生成させることができます。これは、デバッガが MSIL と対応する機械語をマッチさせ、ローカル変数と関数の引数がどこに保存されるか追跡できるようにします。
  • JIT コンパイラに、機械語の最適化を止めさせることができます。

Foo.exe があるなら Foo.ini として

[.NET Framework Debugging Control]
GenerateTrackingInfo=1
AllowOptimize=0

と書いておくと、完全なスタックトレースが取れます!32bit リリースビルドで確認してみました。確かに完全なトレースが取れました。64bit は試してません。
これはリリースファイル作成時に /debug:pdbonly をつけてコンパイルしてあることを前提としているそうです。これはデバッグ向けの機能です(原文でしつこく念押ししているので書いておきます)。

こちらも参考に。
http://www.hanselman.com/blog/DebugVsReleaseTheBestOfBothWorlds.aspx
(追記) http://msdn2.microsoft.com/ja-jp/library/9dd8z24x(VS.80).aspx