はじめに
C#をコンパイルすると中間言語(IL)というものに変換される。
コンパイルでどのような最適化がされるのか知りたいようなときにILを確認できると便利な気がしたので、その方法を調べた。
環境はmac。
事前準備
$ brew install mono
で必要なものは揃うはず。
この記事で試したmonoのバージョンは4.8.0。
デコンパイル
適当にC#のコードを書く。
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としては、以下のようなもので十分そう(クラスすらなくても大丈夫らしい)。
.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でどうなるかを調べた。
static int Func(int x)
{
switch (x)
{
case 0:
return 0;
case 1:
return 1;
default:
return -1;
}
}
これの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を追加してみた。
static int Func(int x)
{
switch (x)
{
case 0:
return 0;
case 1:
return 1;
case 2:
return 2;
default:
return -1;
}
}
これは、以下のようになった。
.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はどうなるかを調べた。
つまり、
static void Func()
{
for (int i = 0; i < 10; i++)
{
int x = i * 2;
Console.WriteLine(x);
}
}
ではxの宣言にコストがあるので、
static void Func()
{
int x;
for (int i = 0; i < 10; i++)
{
x = i * 2;
Console.WriteLine(x);
}
}
と書こうという話である。
それぞれ、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
.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#の事を知ることができた気がする。