はじめに
私は普段Unityを触っているのですが、Unityのライブラリとして有名なUniRx
・UniTask
を作られたneuecc
さんはIL
単位での最適化も行なっているそうです。
いやはや凄すぎて良く分からない世界になってきました。
ただ少しでもC#をうまく活用していくために勉強していきたいと思います。
簡単なコードで試してみる
using System; public class Clinet { public static void Main() { Console.WriteLine(Add(10, 20).ToString()); } public static int Add(int a, int b) { return a + b; } }
↓
.class private auto ansi '<Module>' { } // end of class <Module> .class public auto ansi beforefieldinit Clinet extends [System.Private.CoreLib]System.Object { // Methods .method public hidebysig static void Main () cil managed { // Method begins at RVA 0x2050 // Code size 23 (0x17) .maxstack 2 .locals init ( [0] int32 ) IL_0000: ldc.i4.s 10 IL_0002: ldc.i4.s 20 IL_0004: call int32 Clinet::Add(int32, int32) IL_0009: stloc.0 IL_000a: ldloca.s 0 IL_000c: call instance string [System.Private.CoreLib]System.Int32::ToString() IL_0011: call void [System.Console]System.Console::WriteLine(string) IL_0016: ret } // end of method Clinet::Main .method public hidebysig static int32 Add ( int32 a, int32 b ) cil managed { // Method begins at RVA 0x2073 // Code size 4 (0x4) .maxstack 8 IL_0000: ldarg.0 IL_0001: ldarg.1 IL_0002: add IL_0003: ret } // end of method Clinet::Add .method public hidebysig specialname rtspecialname instance void .ctor () cil managed { // Method begins at RVA 0x2078 // Code size 7 (0x7) .maxstack 8 IL_0000: ldarg.0 IL_0001: call instance void [System.Private.CoreLib]System.Object::.ctor() IL_0006: ret } // end of method Clinet::.ctor } // end of class Clinet
長くなったもんだなぁと思いますが、Add
の中身のみ注目してみましょう。
IL_0000: ldarg.0 IL_0001: ldarg.1 IL_0002: add IL_0003: ret
情報系の学校に通っていた方なんかはもしや?と思うかもしれませんが、あれです。上から命令セットが一個ずつ実行されていく低級言語なやつです。
左についているものはラベルで,上から各命令に番号付けされてます。16進数なので000c
みたいにもなるので注意してください。
今回のサンプルを解読するために、以下の命令を覚えておけばOKです。
命令 | 意味 |
---|---|
ldarg.0 | インデックス 0 の引数を評価スタックに読み込みます。 |
ldarg.1 | インデックス 1 の引数を評価スタックに読み込みます。 |
add | 2 つの値を加算し、結果を評価スタックにプッシュします。 |
ret | 現在のメソッドから戻り、呼び出し先の評価スタックから呼び出し元の評価スタックに戻り値 (存在する場合) をプッシュします。 |
OpCodes クラス (System.Reflection.Emit) | Microsoft Docs
安定のわかりづらいドキュメントですが、スタックをイメージしながら読んでいくと意外とスッキリします。

値型と参照型
ややおまけ的ですが、C#
を触る上で値型と参照型の違いは非常に重要です。
値型はスタック・参照型はヒープとよく聞きますが、実際にメモリ上でどのようになっているかをSharpLab
上で調べることができます。
public class Clinet { public static void Main() { Inspect.Heap(new SampleClass()); Inspect.Stack(new SampleStruct()); } } public class SampleClass { public int X; public long Y; public byte Z; } public struct SampleStruct { public int X; public long Y; public byte Z; }
以下のようなコードを書いた後、Results
をRun
として実行してみてください。

右に表示されたメモリを読んでみると、
class : 22 byte (header:8, type handle:8, X:4, Y:8, Z:1, パディング:3)
struct : 20 byte (X:4, Y:8, Z:1, パディング:7)
注意してほしいのは、Class
をインスタンス化するとデータはヒープに確保されますが、そのヒープへの参照がスタックへと確保されます。

header
とtype handle
はヘッダー領域であり、ヒープの場合先頭に必ず入ってきます(8 + 8 byte)。その後にX, Y, Z
とデータ(4(int) + 8(long) + 1(byte) byte)と続き、最後の3byteはパティングと言われるものみたいです。
正直ヘッダー・パディングについてよくわかっていない(パディングに関しては属性で変更可能?みたい)ので、是非冒頭の動画を一度視聴してみてください。
StructLayoutAttribute クラス (System.Runtime.InteropServices) | Microsoft Docs
さいごに
途中からIL
から離れてしまって申し訳ありませんが、新しい世界が広がっていて面白いですね。
いまだにGC Alloc
やボックス化あたりが分かっていませんが、ここら辺もぼちぼち学べていけたらと思います。
後C#
のILGenerator
を使って最適化とかもしてみたさはあります。
ではまた。