はなちるのマイノート

Unityをメインとした技術ブログ。自分らしくまったりやっていきたいと思いますー!

【C#, IL】C#の中間言語ILを読めるようになりたい(SharpLabの紹介)

はじめに

私は普段Unityを触っているのですが、Unityのライブラリとして有名なUniRxUniTaskを作られたneueccさんはIL単位での最適化も行なっているそうです。

www.youtube.com

いやはや凄すぎて良く分からない世界になってきました。

ただ少しでもC#をうまく活用していくために勉強していきたいと思います。

SharpLab

C#のコードがどのようにILに変換されているかを知るためには、SharpLabというブラウザ上で動作するエディタを活用すると便利です。

sharplab.io

f:id:hanaaaaaachiru:20210727132854p:plain
SharpLab

簡単なコードで試してみる

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


安定のわかりづらいドキュメントですが、スタックをイメージしながら読んでいくと意外とスッキリします。

f:id:hanaaaaaachiru:20210727151955p:plain
処理の流れ

値型と参照型

ややおまけ的ですが、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;
}

以下のようなコードを書いた後、ResultsRunとして実行してみてください。

f:id:hanaaaaaachiru:20210727133755p:plain
ClassとStructのメモリ利用

右に表示されたメモリを読んでみると、

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をインスタンス化するとデータはヒープに確保されますが、そのヒープへの参照がスタックへと確保されます。

f:id:hanaaaaaachiru:20210727141644p:plain
ヒープへの参照がスタックに確保される

headertype 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を使って最適化とかもしてみたさはあります。

docs.microsoft.com


ではまた。