例えば、次のコードの実行結果を Debug ビルドと Release ビルド (非デバッグ実行) とで比較すると一目瞭然だ。
using System;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
Hoge();
Console.ReadLine();
}
static void Hoge()
{
Fuga();
}
[MethodImpl(MethodImplOptions.NoInlining)]
static void Fuga()
{
const int callerFrameIndex = 1;
StackFrame callerFrame = new StackFrame(callerFrameIndex);
MethodBase callerMethod = callerFrame.GetMethod();
Console.WriteLine(callerMethod.Name);
}
}
}
Debug ビルドでは Hoge と出力されるが、Release ビルド (非デバッグ実行) では Main と出力されることが確認できる。つまり、Release ビルド (非デバッグ実行) では JIT 最適化により Hoge メソッドがインライン展開されているわけである。
JIT 最適化によるインライン展開を抑止する方法としては、MethodImpl 属性 を付加して MethodImplOptions.NoInlining を指定するという方法が提供されている。しかし、この方法でインライン展開が抑止されるのは属性が付加されたメソッドのみである。そのため、呼び出し元のメソッド (上記の例なら Hoge メソッド) にこの属性を付加しなければならない。
また、この属性は絶対的なものではなく、64 bit CLR では無視されてしまうらしい。
また、インライン展開の抑止は、C# ・ IL レベルのメソッド呼び出しとコールスタックの一致を保証するわけではない。64 bit - CLR では、インライン展開以外に末尾最適化によっても、メソッド呼び出しとコールスタックの不一致が生じる場合があるのだが、NoInlining では末尾最適化は抑止されない。
デバッグ技 : .ini ファイルによる JIT コンパイラ制御
# このリンク先では ini ファイルを使用して インライン展開を抑止する方法 コールスタックの不一致 を防ぐ方法が紹介されているが、これはあくまでもデバッグ目的の手段である。
では、呼び出し元メソッドのコールスタックを維持する方法は無いだろうか。
実はある。
IL レベルで reqsecobj キーワードが付加されているメソッドでは、呼び出し元メソッドのコールスタックが維持されるのである。 (reqsecobj キーワードが付加されているメソッド自身のコールスタックも維持される。)
C# コンパイラには、このキーワードをメソッドに付加する方法が用意されていて、メソッドに DynamicSecurityMethodAttribute クラス (System.Security) を属性として付加すれば良い。
このクラスは mscorlib.dll の internal クラスであり僕らは本来使用することができないのだが、同名のクラスを自前で用意して使用すれば C# コンパイラは reqsecobj キーワードを付加してくれる。
先ほどのコードに、DynamicSecurity 属性を付けて、再び Release ビルド (非デバッグ実行) で実行してみると、見事にコールスタックが維持されることが確認できる。
using System;
using System.Diagnostics;
using System.Reflection;
using System.Security;
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
Hoge();
Console.ReadLine();
}
static void Hoge()
{
Fuga();
}
[DynamicSecurityMethod]
static void Fuga()
{
const int callerFrameIndex = 1;
StackFrame callerFrame = new StackFrame(callerFrameIndex);
MethodBase callerMethod = callerFrame.GetMethod();
Console.WriteLine(callerMethod.Name);
}
}
}
namespace System.Security
{
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)]
internal sealed class DynamicSecurityMethodAttribute : Attribute
{
}
}
見ての通り、呼び出し元メソッドである Hoge メソッドには属性の付加が一切不要である。
[修正履歴]
2008/10/07
NyaRuRu さんのコメントを元に記事本文を修正。
重要な削除箇所は取り消し線 + 文字色変更 (灰色)。
重要な追記箇所は背景色変更 (緑色)。
目印は付けていないが、何箇所か「インライン展開が抑止」という旨の記述を「コールスタックが維持」という記述に修正してある。
2008/10/08
冒頭の検証用コードで、Fuga メソッドまでインライン展開されてしまいエラーとなっていたので 、Fuga メソッドに MethodImpl(MethodImplOptions.NoInlining) 属性を付加。
>また、この属性は絶対的なものではなく、64 bit CLR では無視されてしまうらしい。
64-bit CLR の Tail-call optimization の話でしたら,あれは「インライン展開ではない」です.なので,NoInlining が無視されてしまうというのもまた違うかと.
要は NoInlining という名前の問題で,あれがたとえば PreserveCallStack という名前であれば「無視されてしまう」でも妥当かと思います.
2008.10.06 11:53 URL | NyaRuRu #1MshU/Gw [ 編集 ]
なるほど、まだ Tail-call optimization について理解しきれていませんが、どういうことかは何と無くわかりました。
つまり、
・Tail-call optimization はインライン展開とは異なるものである
・Tail-call optimization によって (C#・IL レベルでの) メソッド呼び出しとコールスタックが一致しない可能性がある
・NoInlining はインライン展開を抑止するためのものであり、コールスタックの維持は保障しない
って感じでしょうか。
そーすると、僕のこの記事では全体的に「インライン展開=メソッド呼び出しとコールスタックの不一致」として書いていましたが、これは正しくなかったですね orz
後ほど訂正入れるか改訂した記事書きますです。
2008.10.06 13:47 URL | よこけん #Ay6tTHf6 [ 編集 ]
トラックバックURL↓
http://csharper.blog57.fc2.com/tb.php/234-b1395719
末尾最適化を検証
まず、末尾再帰を行う単純な C# コード。Hoge.csusing System;using System.Diagnostics;static class Program{??? static void Main(string[] args)??? {?...
2008.10.09 00:30 | C#と諸々
[C#]Log4Net のラッパーをつくる
備忘録の意味も込めて。 やりたいことは、 Debug 系メソッドはリリースモードでは呼出しごと削除したい いちいち LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); って書くのだるいから省略したい の 2 つ。 Debug 系のメソッドをリリースモードで呼出
2009.11.15 20:42 | 予定は未定Blog版