.NET Frameworkは瓦を積んで、割ってしている事実

スタックというのは瓦で、キューというのは ところてん だ。
まぁなかなかいい例えだと思う。
値を積んでいって上からたたき割るのがスタック、
値を入れて、入れた順番に押し出されてくるのがキュー、
確かに瓦とところてん。
今日はそのスタックの話。


C#で書いたプログラムは当然に.NET Framework上で動くのだが、
実は.NET Frameworkの上で動くプログラムはすべて中間言語でおかれている。
この中間言語と一対一対応した言語があって、それがMSIL。
まぁその辺に転がってるアセンブリ言語と似たような位置づけです。
ただ、結構高機能なんですけどね。
さて、C#でこういう命令を書くことにする。

Console.WriteLine((1+2+3)*(4+5)/(6-7));

実際は定数同士はコンパイル時に計算されるのだが、変数においたりしてそれを阻止したものを作ってみた。
それをMSIL逆アセンブラで開いて、それを参考にMSILで書いてみた。

ldc.i4.1    //[ 1]
ldc.i4.2 // 1 [ 2]
add //[ 3]
ldc.i4.3 // 3 [ 3]
add //[ 6]
ldc.i4.4 // 6 [ 4]
ldc.i4.5 // 6 4 [ 5]
add // 6 [ 9]
mul //[54]
ldc.i4.6 // 54 [ 6]
ldc.i4.7 // 54 6 [ 7]
sub // 54 [-1]
div //[-54]
call void [mscorlib]System.Console::WriteLine(int32)

一行一命令長く付くねぇ。
まぁそれはともかく、ldc.i4.1というのは4bit整数の1を積むという意味。
コメントアウトの部分に積まれてる状況を書いてみた。その命令で積まれたのは[1]のように囲んである。下線の部分は次たたき割られる分。
addでは上2つをたたき割って、そして足したのを積むと。
そんな風になってるわけだ。
なかなか工夫されていて、うまいことやっていって、最後に-54が残ると。
そして、次のSystem.Console.WriteLineを実行するときに、引数として取っていくと。
hpの電卓で、逆ポーランド記法という方法で打ち込んで計算してくれるというのがあるらしい。
それはこの計算をこんな具合で書く。

1 2 + 3 + 4 5 + * 6 7 - /

これと、さっきのMSILの書き方。実は一緒なんですよね。
どうもコンピュータにはかなり理解しやすい書き方みたいです。
括弧とかでグループ化しなくてもうまいこと計算できるってのはすごいですよね。
まぁ人間からすればこの書き方は結構わかりにくいのだけどね。
だからわかりやすい書き方で書いて、それを後ろから前にという調子で書き直してくれているということだな。


いかに難しいプログラムであれ、この方法で全部表されているんだからな。
これは回路計算プログラムの一部。

  .method public hidebysig newslot virtual instance valuetype hidemaro.Complex V(valuetype hidemaro.Complex I) cil managed {
.maxstack 8
ldarg.0
ldarg.1
stfld valuetype hidemaro.Complex hidemaro.Circuit::_I0
ldarg.0
ldarg.1
ldarg.0
callvirt instance valuetype hidemaro.Complex hidemaro.Circuit::get_Z()
call valuetype hidemaro.Complex hidemaro.Complex::op_Multiply(valuetype hidemaro.Complex,valuetype hidemaro.Complex)
stfld valuetype hidemaro.Complex hidemaro.Circuit::_V0
ldarg.0
call instance valuetype hidemaro.Complex hidemaro.Circuit::get_V0()
ret
}

やたら修飾語が多いが、そういうものなのだろう。
ldarg.0は引数の0番をスタックに積むこと。
ところが0番目というのは、インスタンスのメソッドの場合はそのインスタンス自身ということになるらしい。
stfldもそうで、最後に積んだ値を、最後から2番目に積んだインスタンスの変数にセットしますよということ。
クラス名をご丁寧に書いてくれてあるが、やはりそういうものだろう。

stfld( hidemaro.Circuit.I0 , (hidemaro.Circuit)arg[0] , (hidemaro.Complex)arg[1] );

まぁこれならオブジェクト指向でも何でもないわけだけど、そういうもんです。
逆はldfldで、これは最後に積んだインスタンスの変数を拾ってくると言うこと。
ldarg.0がやたら多いのは、オブジェクトが中心になって動いてるからですね。
プロパティのゲッター・セッターはきちんとメソッドに書き換えられてますね。
だからプロパティはやはりメソッドで、変数は変数で、見た目は似てても全然違うということです。
だからあらかじめプロパティを使おうという話になるわけだな。こうしておけばかなり安心ですよね。
callvirtとcallの違いというのもやはりありますね。
callvirtは仮想メソッド・抽象メソッドの場合に使う。callはそれ以外のメソッドで使う。
まぁそんなあたりからも、あらかじめvirtual修飾子が必要なんだとわかりますね。


  .method public hidebysig instance bool ReadLine(string line) cil managed {
.maxstack 2
.locals init (/*2つ省略*/,[2] string op,[3] string val,[4] bool finished)
ldarg.0
ldfld class hidemaro.PerserableCircuit hidemaro.PerserableCircuit::cur
brtrue JP1
/* --中略 -- */
ldloc.2
ldstr "End"
call bool [mscorlib]System.String::op_Equality(string,string)
brfalse.s JP3
ldc.i4.1
ret
JP3:
/* --中略-- */
JP6: ldc.i4.0
ret
JP1: ldarg.0
ldfld class hidemaro.PerserableCircuit hidemaro.PerserableCircuit::cur
ldarg.1
callvirt instance bool hidemaro.PerserableCircuit::ReadLine(string)
stloc.s finished
ldloc.s finished
brfalse.s JPA
ldarg.0
ldnull
stfld class hidemaro.PerserableCircuit hidemaro.PerserableCircuit::cur
JPA: ldc.i4.0
ret
}

かなり省略したのですが…なんせ長いので。
あと、MSIL自身は全部の命令に対してIL_xxxxというラベルを付けてたのだが、必要な分だけJPxに書き換えました。
brtrue.s・brfalse.sは最後に積まれたのが真か偽か判断して飛ぶ、br.sは無条件で飛ぶ、そういうことだと思います。
ldloc.2で、2番のローカル変数の値を積む。このローカル変数の宣言は.localsでしているわけだ。
一方では最後の方で、stloc.s finishedというのがあるが、これは名前で言ってるな。stloc.3まではあるけど、4はないんです。
これで4番のローカル変数、finishedに最後に積まれた値がセットされると。
文字列の比較でおもしろいと思ったのは、ldloc.2とldstr “R”で、2つの値を積む。
そこでbool [mscorlib]System.String::op_Equalityop_Equality(string,string)を呼び出す。
これは==という演算子そのものなのだが、数値以外はこうやって置き換えられるわけね。
そして、2つの値を取って、真か偽かが積まれると。なかなかうまいことできてますね。


ところで、一回しか使わない値でも取り扱いの都合変数に入れることはよくあります。
離れたところで使いたいとか。
あとうまく書けないから、一回変数に入れるとか。
ここでは非常に残念なことに、stloc.s finishedの直後にldloc.s finishedが呼ばれると言うことで、変数の意味がないと。
まぁちょっと残念ですね。
他にも工夫をすれば変数いらんのも結構ありました。
まぁ正直言うと、変数に一旦入れるにせよ、あまり性能は変わらないはずなんだけどね。
しかし、Z80のハンドアセンブリしてたときも、LDを何度も書くのはめんどくさいなぁと思って、下品なプログラムを作ってた覚えがある。
まぁC#のソースを見てるだけじゃなんとも思わないけど、結構いろいろあるもんです。
ちょっとは機械に近いところから見てみるのもおもしろいかもしれない。
見るだけだけどね。こんなので作る気はあまり起きない。