渋谷ほととぎす通信

完全趣味でやってるUnityメモ。説明できないところを説明できるようにするための個人ブログ。昨日の自分より少しでも大きくなれるように。。。 ※所属団体とは一切関係がありません

初心者向け事前知識なしの状態からUnityエンジニアがILを読んでみる


f:id:esakun:20181102095820p:plain:w450

先日アセンブリを読むという記事を書き、C#から(コンパイラから)出力されたコードが最近の僕のブームでして、ILも一応読んで、Hello world ILくらいの勉強はしておこうと思います。

お品書き

その1.超シンプルなC#のILを読む

以下のコードのILとして出力して、読んでみようと思います。

※C#からのIL出力に関してはCSharpLabを利用させてもらっています。

class Csharp 
{
  public void Main() {}
}


ILに出力するとこうなります。
※適宜コメントを書き込んでいます

3行のC#コードがコメント除いて20行くらいのILに出力されるということ自体がびっくりですが、見ていきます。

IL命令の文法

IL_0000: ldarg.1

このIL_0000:がラベルで、ldarg.1が処理内容になります。

この場合、関数の第1引数をスタックに乗せるという意味になります。

4行目 '<Module>'というものが自動生成

ググってもあまり出てこなくて、一旦ステイしていますが、Moduleというものが自動生成されます。すみません、分かりません。

9行目 System.Objectが継承された形でクラス定義される

.class private auto ansi beforefieldinit Charp extends [mscorlib]System.Object

C#のクラスはすべてSystem.Objectを継承していますが、省略することもC#上では可能です。
考えてみれば当然ですが、ILに吐き出される時にはそれがくっついた形になります。

11行目 メソッド定義

.method public hidebysig instance void Main () cil managed 

Mainという名前はそのままILにもMainと記述されるようです。
処理があろうがなかろうが、メソッドは最終IL_0000: ret(returnの意)という処理で終了します。

18行目.コンストラクタが自動生成される

コンストラクタを記述していないとIL側で自動生成されるようです。

.method public hidebysig specialname rtspecialname instance void .ctor () cil managed 

コンストラクタではCsharpクラスの親クラスに当たるSystem.Objectクラスのコンストラクタが呼ばれています。ctor()がコンストラクタに当たります(たぶん)。

その2.超シンプルな足し算のC#のILを読む

class Csharp
{
    public int Main(int piyo, int bar, 
                              int foo, int hoge, int piyopiyo) 
    {
        return piyo + bar + foo + hoge + piyopiyo;
    }
}

次にこのような第1〜第5int型引数をすべて加算してreturnする関数のILを読んでみます。


出力された、関数部分のILはコチラ。
※フル出力のILはコチラで確認できます。

ほとんどコード内のコメントアウトに処理内容を記載しているのでわかるかと思います。

気づいたポイントだけメモしておきます。

メソッド引数名は使用されないと思ったら第4引数から変数名を使用していた

piyo、bar、foo、hoge、piyopiyoとメソッド引数を宣言しましたが、それらは、ldarg.1ldarg2という命令文でスタックに乗るという処理として置き換わっていますが、第4引数からはそのルールが変わり、ldarg.s 変数名という処理に変わっていることに注意です。

add命令は2つの値を加算する

5つの値を同時に加算することはできず、4回に分けて加算されていることがわかります。

その3.フィールド変数

フィールド変数を使用するとILはどう変化するのか見てみます。

class Csharp
{
    string instanceVaribale = hoge;
    
    public void Main() 
    {
        System.Console.WriteLine(instanceVaribale);
    }
}


出力された、関数部分のILはコチラ。
※フル出力のILはコチラ

気づいたポイントをメモします。

フィールドにアクセスするためには自分の参照が必要

ローカル変数と違い、インスタンス変数を参照するときには必ず自分自身の参照をスタックに乗せる必要があるということがわかります。

9行目辺り

// 自分自身の参照をスタックに乗せる
IL_0000: ldarg.0

// instanceVaiableの値をスタックに乗せる
IL_0001: ldfld string Csharp::instanceVaribale

ldfldは次のように説明されています。

参照が現在評価スタック上にあるオブジェクト内のフィールドの値を検索します。

OpCodes.Ldfld フィールドとは - .NET Framework クラス ライブラリ リファレンス Weblio辞書より 

また、スタックの流れとして以下のようにも説明されています。

1.オブジェクト参照 (またはポインタ) がスタックにプッシュされます。
2.オブジェクト参照 (またはポインタ) がスタックからポップされます。オブジェクト内の指定したフィールドの値が検索されます。
3.フィールドに格納されている値がスタックにプッシュされます。

OpCodes.Ldfld フィールドとは - .NET Framework クラス ライブラリ リファレンス Weblio辞書より 

  • 「1.」は、サンプルで言うところの 10行目 IL_0000: ldarg.0 自分自身の参照がスタックにプッシュされるということでしょう。
  • 「2.」は、自分自身の参照を取得(ポップ)し、自分自身(オブジェクト)内の指定したフィールド 11行目のCsharp::instanceVaribaleの値が検索される。※ここでは"hoge"が検索された
  • 「3.」は、"hoge"がスタックにプッシュされる

という流れになると思われます。

フィールド変数に初期値を入れていた場合はコンストラクタで処理されている

フィールド変数を定義したタイミングで初期値を入れることもあると思いますが、初期値はコンストラクタ内で処理されていることがわかりました。

その4.フィールド変数初期値をコンストラクタで上書きしたら?

この辺から余談になります。

class Csharp
{
    string instanceVaribale = "hoge";
    public Csharp(){
        instanceVaribale = "foo";
    }
}

このようにコンストラクタ内でフィールド変数の初期値を上書きするとILはどう変化するか。

フル出力はコチラのようになります

このように、コンストラクタ内で2回同じインスタンスに値をセットする処理が入ることになるため、無駄なコードということがわかりました。

余談 C#コンパイラが最適化してくれるのでは?

してくれないようです。


以上。

参考