[cs] /language 又はビルドペインの詳細設定中にある言語バージョンに関して注意すべきforeachの挙動
このエントリは、2012/08/31現在に、実際に動かした結果に関する考察で有り、それ以上の検証は行ってないのでその点ご了承の程を。
どー言うことなのか
VS2010迄は*1、λ内でforeachの要素をキャプチャする際、以下のようなサンプルを書いた場合、直感的では無い結果になってました。
using System; using System.Collections.Generic; namespace ConsoleApplication1 { class Program { static void Main(string[] args) { IEnumerable<int> ite = new int[] {0, 1, 2, 3, 4}; Action action = null; foreach (var i in ite) { action += () => Console.WriteLine(i); } action(); Console.ReadLine(); } } }
このアウトプットは
4 4 4 4 4
しかしVS2012で上記コードを実行すると
0 1 2 3 4
となり、大方の場合、意図通りになります。
で、この辺の詳細に関しては今回割愛させていただきます。興味を持たれた方は、C#言語仕様 Version5.0のP279に記載があるのでご覧頂ければと思います。
何を注意すべきなのか
コマンドラインでC#のコンパイラを叩いた場合/languageオプションで又はVS上ではプロパティにある、ビルドペイン最下の詳細設定で言語バージョンを指定出来ます。この場合、個人的には、出力結果も従前と同じように4の羅列になると考えていたのですがさに非ず、結果は変更後のC#5.0と同じ結果になりました。*2
MSDNでこの辺に関するコトをつらつらと探していたんですが、わかったことは/languageの解説にせよ、詳細設定の言語バージョンの解説にせよコンパイラが受容する構文規則が変化するとだけ書いてあり、従前のコンパイラと同様の出力結果を返すとの記載は見当たりませんでした。
従って、従前と同様の挙動をさせたいと言う意図で、言語バージョン又は/languageを変更しても望む結果を得ることは出来ないので注意すべきかなと。*3
VisualStudio 2012にCodeContractsを導入する方法
よーやく導入したは良いけど、結構どころで無く表題に詰まった。
コード分析≠Code Contracts
エディション間の比較とか見てて、これを完全に勘違いしていたのがそもそもの原因。
あれは、コーディング規約とかの分析でCodeCopっぽいものだった。*1なので、あそこをいくらいじったところでCode Contractsとは全く関係なかったりする。
じゃあ実際どのようにして導入するか
と言うと、
http://social.msdn.microsoft.com/Forums/pl-pl/codecontracts/threads
ここの”Announcements”に載ってるとおりで、
- VS2012のインストールを行う
- CodeContractsから今日現在*2Release 1.4.50813.1 を持ってきてインストールする
- 管理者モードでコマンドプロンプトを起ち上げる
- VS2012のdevenv.exeがあるフォルダにカレントディレクトリを移動させる
- devenv /setupを実行する
- しばし待つ
と言う手順が必要。
この手順が成功すれば、プロジェクトのプロパティに見慣れたCode Contractsペインが出現して後はいつも通りに使える。
恐らく後日またやるハメになる可能性が高いので備忘録的に。。。
delegateに代入された特定条件を満たしたラムダに関する一考察。
ある方に相談したら、何となくわかってきたので、後でまた忘れたときのために。
で、満たすべき条件とは
- 閉包*1を使ってない
と言う点、この条件が意味するところは、ラムダ式をStatic methodとしてコンパイル可能であるという点。逆に、閉包を使ってないのなら、ブロックにしてしまって、複数行にわたって書いても構わないみたい。*2。
この条件を満たしたサンプルは以下の通り
using System; namespace ConsoleApplication1 { internal class Program { private static void Main(string[] args) { Func<int, int, int> add = (x, y) => x + y; Action<int> twiceAndPrint = _ => Console.WriteLine(_*2); var result = add(10, 20); twiceAndPrint(result); } } }
さて、こいつをコンパイルしたとき、cscはどのようなILを吐くのか調べてみたところ、C#で、再構築してみると概ね以下のような形になった。
using System; namespace ConsoleApplication1 { internal class Program { private static Func<int, int, int> CachedAnonymousMehod0; private static Action<int> CachedAnonymousMehod1; private static void Main(string[] args) { //隠しキャッシュがnullだったら、登録する。 if (CachedAnonymousMehod0 == null) { CachedAnonymousMehod0 = new Func<int, int, int>(AddImpl); } if (CachedAnonymousMehod1 == null) { CachedAnonymousMehod1 = new Action<int>(TwiceAndPrintImpl); } //Func<int, int, int> add = (x, y) => x + y; Func<int, int, int> add = CachedAnonymousMehod0; //Action<int> twiceAndPrint = _ => Console.WriteLine(_*2); Action<int> twiceAndPrint = CachedAnonymousMehod1; //var result = add(10, 20); int result = add(10, 20); //twiceAndPrint(result); twiceAndPrint(result); } //(x, y) => x + y private static int AddImpl(int x, int y) { return x + y; } //_=>Console.WriteLine(_*2) private static void TwiceAndPrintImpl(int _) { Console.WriteLine(_*2); } } }
実際には、ラムダ式の部分は、そのクラスのprivate staticなメソッドに展開され、デリゲートのキャッシュを持つことになる。
じっさいここまでは、以前から調べは付いていたけど、何故またキャッシュするのか?と言う点に関して、尤もらしい理由を付けることが出来なかった*3。
で、先に戻って、ある方に相談してその会話の中でふと思いついたのが、
- 実装の単純性
って点だった。これは何かというと、恐らく、コンパイラがキャッシュを生成してデリゲートの構築コストを省こうとするコンパイル結果を生成する理由として、参照先のラムダ式は概ね単純だろうと言う仮定に基づいているのでは無いかなと。
思い返すと、Func
この場合、もしかしたら
- デリゲート経由で実装を呼び出して結果を返す時間的コスト<デリゲートを1回1回キャッシュ無しで生成する時間的コスト
となり得ることが多いと仮定して、もしかしたら繰り返しエンクロージャ側のメソッドが呼ばれるコトを想定して、キャッシュしてるのでは無いかなと*5
逆に、一般的なメソッドを参照させるときは、別のメソッドとして存在してるくらいだから、それなりに中でやってることも複雑だろう、時間がかかるだろうと言うことと、恐らく、静的メソッドと、インスタンスメソッドを分けて考えるようなことはしないために、キャッシングしないのじゃ無いかと思う。
と言うことで、備忘録的なメモ。
反変性と共変性
ここ数日、こいつで引っかかったけど、ようやく理解できたので備忘録。
コトの発端
こんなコードを書いていた。
interface ISample<in T> { void InvokeAction(Action<T> action); }
そしたら、コンパイラに怒られた。
無効な分散: 型パラメーター 'T' は、'ConsoleApplication1.ISample<T>.InvokeAction(System.Action<T>)' で有効な 共変的 である必要があります。'T' は 反変 です。
これが直感的に理解できなかったってのがコトの発端でして。
何故これがダメなのか?
これが許容されると、こんなワケのわからないコードがコンパイル可能と言うことに帰結する。
internal class Program { private static void Main(string[] args) { ISample<string> sample = new Sample<object>(200); //ちょっとマテ、元はobjectだし、実際入ってるのはintだし、これは拙くなイカ? sample.InvokeAction((string value) => Console.WriteLine(value.Substring(2))); } } internal interface ISample<in T> { void InvokeAction(Action<T> action); } class Sample<T> : ISample<T> { public Sample(T value) { Value = value; } public T Value { get; set; } public void InvokeAction(Action<T> action) { action(Value); } }
Actionの型パラメタは反変だから、インターフェースの型パラメタも反変なら、このように書けるだろうと、直感的に思いがちだけど、さに非ず、実際は反変の型パラメタを持つActionを使いたいのなら、インターフェースの型パラメタは以下のように共変である必要がある。
internal class Program { private static void Main(string[] args) { ISample<object> sample = new Sample<string>("hello world"); //これなら、常にアップキャストが保障されるので、大丈夫 sample.InvokeAction((object value) => Console.WriteLine(value)); } } internal interface ISample<out T> { void InvokeAction(Action<T> action); } internal class Sample<T> : ISample<T> { public Sample(T value) { Value = value; } public T Value { get; set; } public void InvokeAction(Action<T> action) { action(Value); } }
よくよく考えれば当たり前だけど、引っかかったので、備忘録的に。
ILレベルでのデバッグ方法
そうそう使わないけど、逆アセンブラとの兼ね合いで欲しくなることもあるので、備忘録的に。。。
やり方は、難しくは無いけど若干面倒。仕込み方は以下の通り。
- VisualStudio コマンドプロンプトを起動する
- Current directoryをコンパイルの成果物があるDirectoryに移動させる
- 仮に、成果物がhoge.exeの場合、まず最初にdasmにかけて、ilのソースコードを得る。"ildasm hoge.exe /OUT=hoge.exe.il /SOURCE"
- 次に、先に得たilをilasmに書けてpdb付きでアセンブルする。"ilasm hoge.exe.il /DEBUG"
- デバッグ用のダミープロジェクトをでっち上げる*1
- ダミープロジェクトのプロパティでデバッグペインを開き、開始動作を”外部プログラムの開始”にして、先の"hoge.exe.exe"を指定する。
- デバッグ開始
これでILレベルでステップデバッグが効く。
有りモノのコンパイル済み生成物を1回dasmして,もういっかい、アセンブルし直す理由は、ilベースのpdbファイルが欲しいから。また、ilasmのオプションを変更することで、様々な条件でデバッガを動かせる(と思う。)
若干蛇足だけど、ASMまで降りていく場合、ilasmで/debug=OPTを指定しても、デバッガ側の”モジュール読み込み中にJIT最適化を抑制する(マネージのみ)”のチェックを外しておかないと、恐らく、最適化されてないASMが生成されるような気がする*2
ホントに、tail.は遅いのか?
前回、前々回とF#や、C#、果てはILまで追っかけつつ検証してきた、tail.なワケですが、tail.有りと無しで果たしてどれほどの差があるのか?
んでもって、条件により結果は変わってくるのか?てーコトを検証してみた。
ただ、C#では例え末尾再帰に合致したとしても、tail.プレフィクス付きのコンパイルをしてくれないので、条件を合わせるため、DynamicMethodとして、検証対象を生成した上で検証してマス
tail.無しのサンプルメソッドは以下の通り
static Func<long,long,long> CreateNonTailOptimizedFunction() { var ret = new DynamicMethod("NonOptimized", typeof (long), new[] {typeof (long), typeof (long)}); var gen = ret.GetILGenerator(); var lbl = gen.DefineLabel(); gen.Emit(OpCodes.Ldarg_1); gen.Emit(OpCodes.Brtrue, lbl); gen.Emit(OpCodes.Ldarg_0); gen.Emit(OpCodes.Ret); gen.MarkLabel(lbl); gen.Emit(OpCodes.Ldarg_0); gen.Emit(OpCodes.Ldarg_1); gen.Emit(OpCodes.Add); gen.Emit(OpCodes.Ldarg_1); gen.Emit(OpCodes.Ldc_I8, 1L); gen.Emit(OpCodes.Sub); gen.Emit(OpCodes.Call, ret); gen.Emit(OpCodes.Ret); return (Func<long, long, long>) ret.CreateDelegate(typeof (Func<long, long, long>)); }
冗長だけど、tail.付きのサンプルメソッドは以下の通り。
static Func<long,long,long> CreateTailOptimizedFunction() { var ret = new DynamicMethod("Optimized", typeof(long), new[] { typeof(long), typeof(long) }); var gen = ret.GetILGenerator(); var lbl = gen.DefineLabel(); gen.Emit(OpCodes.Ldarg_1); gen.Emit(OpCodes.Brtrue, lbl); gen.Emit(OpCodes.Ldarg_0); gen.Emit(OpCodes.Ret); gen.MarkLabel(lbl); gen.Emit(OpCodes.Ldarg_0); gen.Emit(OpCodes.Ldarg_1); gen.Emit(OpCodes.Add); gen.Emit(OpCodes.Ldarg_1); gen.Emit(OpCodes.Ldc_I8, 1L); gen.Emit(OpCodes.Sub); gen.Emit(OpCodes.Tailcall); gen.Emit(OpCodes.Call, ret); gen.Emit(OpCodes.Ret); return (Func<long, long, long>)ret.CreateDelegate(typeof(Func<long, long, long>)); }
最後に、検証用のドライバメソッドは以下の通り。
static void Main(string[] args) { var n = 5000000; var optimized = CreateTailOptimizedFunction(); var nonOptimized = CreateNonTailOptimizedFunction(); Console.WriteLine(optimized(0, n)); Console.WriteLine(nonOptimized(0, n)); GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); var round = 10; var optimizedLst = new TimeSpan[round]; var nonOptimizedLst = new TimeSpan[round]; var watch = new Stopwatch(); for (int i = 0; i < round; i++) { watch.Restart(); optimized(0, n); watch.Stop(); optimizedLst[i] = watch.Elapsed; } for (int i = 0; i < round; i++) { watch.Restart(); nonOptimized(0, n); watch.Stop(); nonOptimizedLst[i] = watch.Elapsed; } //ここから以下は、結果をCSVに書き出すための処理。 var wtr = new data::CsvWriter("result.csv", "Round", "n", "NonOptimized", "Optimized"); var descriptor = wtr.AddDescriptor<TimeSpan>(); descriptor.RegistPresenter(arg => arg.TotalMilliseconds); optimizedLst.Zip(nonOptimizedLst, (a, b) => new {Optimized = a, NonOptimized = b}) .ForEach((elem, arg) => { wtr.Write(arg.ToString()); wtr.Write(n.ToString()); wtr.Write(elem.NonOptimized); wtr.Write(elem.Optimized); }); wtr.Close(); }
で、こいつらを検証していくワケですが、デフォルトのスタックサイズだと、差が出にくいので、editbin.exeで、スタックサイズを250MBに設定し直しているので、その点ご了承の程。
tail.の復活
昨日のエントリで、いらない子扱いされてしまって、枕に涙を塗らしていた*1tail.プレフィクス。F♯のコンパイラオプションでは明示しなければ使ったもらえず、以下のようなコードの場合でも、スタックフレームを1個除去できるかどうかだけで、ほぼあんたの居場所ね−からと言われかねない状況だった。
let tailCallSum n= let rec sum accum n=if n=0L then accum else sum (n+accum) (n-1L) sum 0L n;; printfn "%d" (tailCallSum 100);;
この場合、再帰するのは内側の”sum”であり、これを、C♯風に無理矢理かくってーと以下のような感じ*2
using System; static class Program { static void Main() { //やってみた感じ、tail.が適用出来そうな場合でも、 //こっちの呼び出しには、tail.は付かない模様。 Console.WriteLine(tailCallSum(100L)); } static long tailCallSum(long n) { //この呼び出しが、tail.になるかどうかだけ。 return sum(0, n); } static long sum(long accum, long n) { //引数を使っちまってますが、その辺はご愛敬。 //モノの見事にループ展開されている。 while (n != 0L) { accum += n--; } return accum; } }
これからわかるように、外側の"tailCallSum"が内側の"sum"を呼び出すときに、tail.が付くかどうかと言うだけで、sumの内部は、昨日と同様なループ展開がされていると。
こうなってしまうと、tail.のありがたみはほぼほぼ無いと言えなくも無い。
復活の狼煙
このまま、本当にいらない子になってしまうのか?と言うと、さに非ず。相互再帰の場合、生きてくるコトがある。
相当に作為的だけど、この関数を相互再帰で書き直してみると以下の通り。
let rec mutalSumA accum n=if n=0L then accum else mutalSumB (n+accum) (n-1L) and mutalSumB accum n=if n=0L then accum else mutalSumA (n+accum) (n-1L);; printfn "%d" mutalSumA 0 100L;; printfn "%d" mutalSumB 0 100L;;
中身は、ご覧の通り全く同じコトをしてるmutalSumAとmutalSumBを相互的に呼び出している。で、このコンパイル結果を先ほどの通り、意訳してC#で書くと以下の通り。
using System; static class Program { static void Main() { //やってみた感じ、tail.が適用出来そうな場合でも、 //こっちの呼び出しには、tail.は付かない模様。 Console.WriteLine(mutalSumA(0L, 100L)); Console.WriteLine(mutalSumB(0L, 100L)); } static long mutalSumA(long accum,long n) { if (n == 0) return accum; return mutalSumB(accum + n, --n); } static long mutalSumB(long accum, long n) { if (n == 0) return accum; return mutalSumA(accum + n, --n); } }
先の例とは異なり、ループ展開されていない。
と言うことは・・・*3
当然、スタックオーバーフローを起こす程度にでかい数食わせてみたくなる。
あたしの環境*4だと、概ね100,000食わせると、落ちてくれることがわかってるので,コンパイラオプションの“tail呼び出しの生成”の有無で実行結果に差が出るか、調べてみた。
let rec mutalSumA accum n=if n=0L then accum else mutalSumB (n+accum) (n-1L) and mutalSumB accum n=if n=0L then accum else mutalSumA (n+accum) (n-1L);; printfn "%d" mutalSumA 0 100000L;; printfn "%d" mutalSumB 0 100000L;;
その結果は、予想通り、“tail呼び出しの生成”がチェックされている場合は、何の問題も無く、実行された。けどチェックを外した場合、StackOverFlowExpceptionが発生してこけた。