LoginSignup
12
6

More than 5 years have passed since last update.

monoでC#のILを確認してみる

Last updated at Posted at 2017-05-09

はじめに

C#をコンパイルすると中間言語(IL)というものに変換される。
コンパイルでどのような最適化がされるのか知りたいようなときにILを確認できると便利な気がしたので、その方法を調べた。
環境はmac。

事前準備

$ brew install mono

で必要なものは揃うはず。
この記事で試したmonoのバージョンは4.8.0。

デコンパイル

適当にC#のコードを書く。

Program.cs
using System;

class MainClass
{
    public static void Main(string[] args)
    {
        Console.WriteLine("hello");
    }
}

コンパイルする。

$ mcs Program.cs

これにより、Program.exeが作られる。
Program.exeの中身はオブジェクトコードと呼ばれ、バイナリなので、人が読める形のILにデコンパイルしたい。
これにはmonodisを使う。

$ monodis Program.exe

手元の環境では、以下のようなILが出力された。

.assembly extern mscorlib
{
  .ver 4:0:0:0
  .publickeytoken = (B7 7A 5C 56 19 34 E0 89 ) // .z\V.4..
}
.assembly 'Program'
{
  .custom instance void class [mscorlib]System.Runtime.CompilerServices.RuntimeCompatibilityAttribute::'.ctor'() =  (
        01 00 01 00 54 02 16 57 72 61 70 4E 6F 6E 45 78   // ....T..WrapNonEx
        63 65 70 74 69 6F 6E 54 68 72 6F 77 73 01       ) // ceptionThrows.

  .hash algorithm 0x00008004
  .ver  0:0:0:0
}
.module Program.exe // GUID = {D15CB2F0-5E55-443C-A977-C6DE47EB30C8}


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

    // method line 1
    .method public hidebysig specialname rtspecialname 
           instance default void '.ctor' ()  cil managed 
    {
        // Method begins at RVA 0x2050
    // Code size 7 (0x7)
    .maxstack 8
    IL_0000:  ldarg.0 
    IL_0001:  call instance void object::'.ctor'()
    IL_0006:  ret 
    } // end of method MainClass::.ctor

    // method line 2
    .method public static hidebysig 
           default void Main (string[] args)  cil managed 
    {
        // Method begins at RVA 0x2058
    .entrypoint
    // Code size 11 (0xb)
    .maxstack 8
    IL_0000:  ldstr "hello"
    IL_0005:  call void class [mscorlib]System.Console::WriteLine(string)
    IL_000a:  ret 
    } // end of method MainClass::Main

  } // end of class MainClass

なんとなく、"hello"という文字列をロードして、System.Console::WriteLineという関数を呼んでいるというようなことが分かる。

ILの読み方

https://codezine.jp/article/detail/2624
が参考になった。
どんな命令があるかの一覧はどこを見ればいいか分からなかったが、
https://msdn.microsoft.com/ja-jp/library/system.reflection.emit.opcodes(v=vs.110).aspx
は参考になりそう。

ILからオブジェクトコードへの変換

逆に、ILからオブジェクトコードに変換したい場合は、ilasmを使う。

$ ilasm Program.il

実は、helloを出力するILとしては、以下のようなもので十分そう(クラスすらなくても大丈夫らしい)。

Program.il
.assembly extern mscorlib { }
.assembly Program { }

.method static void Main() cil managed {
    .entrypoint
    .maxstack 8
    ldstr "hello"
    call void [mscorlib]System.Console::WriteLine(string)
    ret
}
$ ilasm Program.il
Assembling 'Program.il' , no listing file, to exe --> 'Program.exe'

Operation completed successfully
$ mono Program.exe
hello

ILから読み解く最適化の実例

swtichの最適化

@neueccさんの記事で
http://engineering.grani.jp/entry/2017/02/20/175816
というのがあって、これが実際に手元のmonoでどうなるかを調べた。

Program.cs
    static int Func(int x)
    {
        switch (x)
        {
            case 0:
                return 0;
            case 1:
                return 1;
            default:
                return -1;
        }
    }

これのILを出力すると、以下のようになった。

Program.il
   .method private static hidebysig 
           default int32 Func (int32 x)  cil managed 
    {
    // Method begins at RVA 0x2061
    // Code size 24 (0x18)
    .maxstack 8
    IL_0000:  ldarg.0 
    IL_0001:  brfalse IL_0012

    IL_0006:  ldarg.0 
    IL_0007:  ldc.i4.1 
    IL_0008:  beq IL_0014

    IL_000d:  br IL_0016

    IL_0012:  ldc.i4.0 
    IL_0013:  ret 
    IL_0014:  ldc.i4.1 
    IL_0015:  ret 
    IL_0016:  ldc.i4.m1 
    IL_0017:  ret 
    } // end of method MainClass::Func

brfalseというのが独特という感じがするが、確かにifの羅列というようなコードになっている。
もう一つcaseを追加してみた。

Program.cs
    static int Func(int x)
    {
        switch (x)
        {
            case 0:
                return 0;
            case 1:
                return 1;
            case 2:
                return 2;
            default:
                return -1;
        }
    }

これは、以下のようになった。

Program.il
    .method private static hidebysig 
           default int32 Func (int32 x)  cil managed 
    {
    // Method begins at RVA 0x2061
    // Code size 31 (0x1f)
    .maxstack 8
    IL_0000:  ldarg.0 
    IL_0001:  switch (
      IL_0017,
      IL_0019,
      IL_001b)
    IL_0012:  br IL_001d

    IL_0017:  ldc.i4.0 
    IL_0018:  ret 
    IL_0019:  ldc.i4.1 
    IL_001a:  ret 
    IL_001b:  ldc.i4.2 
    IL_001c:  ret 
    IL_001d:  ldc.i4.m1 
    IL_001e:  ret 
    } // end of method MainClass::Func

swtichという命令が使われていて、確かに効率的っぽい感じになっている。
他にも、試しに文字列のswitchのILを見てみたら、内部的にDictionaryが作られるようなコードになった。
これは、@neueccさんの記事とは違う結果であったが、文字列の比較を愚直に直列でやってなさそうということで一安心した。

forの中での変数宣言

forの中で変数宣言すると、ループする度にオーバーヘッドが発生するから良くないとかいう都市伝説があったりするが、実際にILはどうなるかを調べた。
つまり、

ForTest1.cs
    static void Func()
    {
        for (int i = 0; i < 10; i++)
        {
            int x = i * 2;
            Console.WriteLine(x);
        }
    }

ではxの宣言にコストがあるので、

ForTest2.cs
    static void Func()
    {
        int x;
        for (int i = 0; i < 10; i++)
        {
            x = i * 2;
            Console.WriteLine(x);
        }
    }

と書こうという話である。

それぞれ、ILに変換してみると、

ForTest1.il
    .method private static hidebysig 
           default void Func ()  cil managed 
    {
    // Method begins at RVA 0x2060
    // Code size 30 (0x1e)
    .maxstack 2
    .locals init (
        int32   V_0,
        int32   V_1)
    IL_0000:  ldc.i4.0 
    IL_0001:  stloc.0 
    IL_0002:  br IL_0015

    IL_0007:  ldloc.0 
    IL_0008:  ldc.i4.2 
    IL_0009:  mul 
    IL_000a:  stloc.1 
    IL_000b:  ldloc.1 
    IL_000c:  call void class [mscorlib]System.Console::WriteLine(int32)
    IL_0011:  ldloc.0 
    IL_0012:  ldc.i4.1 
    IL_0013:  add 
    IL_0014:  stloc.0 
    IL_0015:  ldloc.0 
    IL_0016:  ldc.i4.s 0x0a
    IL_0018:  blt IL_0007

    IL_001d:  ret 
    } // end of method MainClass::Func
ForTest2.il
    .method private static hidebysig 
           default void Func ()  cil managed 
    {
    // Method begins at RVA 0x2060
    // Code size 30 (0x1e)
    .maxstack 2
    .locals init (
        int32   V_0,
        int32   V_1)
    IL_0000:  ldc.i4.0 
    IL_0001:  stloc.1 
    IL_0002:  br IL_0015

    IL_0007:  ldloc.1 
    IL_0008:  ldc.i4.2 
    IL_0009:  mul 
    IL_000a:  stloc.0 
    IL_000b:  ldloc.0 
    IL_000c:  call void class [mscorlib]System.Console::WriteLine(int32)
    IL_0011:  ldloc.1 
    IL_0012:  ldc.i4.1 
    IL_0013:  add 
    IL_0014:  stloc.1 
    IL_0015:  ldloc.1 
    IL_0016:  ldc.i4.s 0x0a
    IL_0018:  blt IL_0007

    IL_001d:  ret 
    } // end of method MainClass::Func

となった。
これを見ると、前者ではILのローカル変数V_0がC#の変数i、V_1がxに対応しているのに対して、後者ではその対応が逆になっている。
しかしそれ以外は全く同じILが生成されているため、実行速度に違いは生じないと考えられる。
この事から、xのスコープが小さくなり保守性が高まる前者の書き方を採用すればいいという事が分かる。
この都市伝説は何だったのか…。

まとめ

monoでC#のILを確認する方法をまとめた。
少しC#の事を知ることができた気がする。

12
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
6