心を決めて IL をやる #1

IL(アイエル)をご存知でしょうか。

そう、C#などの言語とマシン語命令の間にあるやや機械よりの中間言語のことです。

C#などの.NETの言語はコンパイルされるとILに変換され、 実行時に実際のマシン語命令に変換されます。

なので、通常開発者がILを意識する必要はないのですが .NETを深く理解したい勇気ある開発者はILも(ある程度)理解しているものです。

こうしてブログを書いている私も勇気を出して、 難攻不落なILに一歩踏み出します。ご一緒にいかがですか(´;ω;`)

さあ、行きますよ!

さっそくILに変換してみる

難しいことは簡単なところから攻めるのがセオリーです。

以下のコードは Main 関数内で変数a1+1の結果を代入し、 Console.WriteLineメソッドで変数aの内容を表示するというこれ以上ないくらいシンプルなものです。

using System;

namespace ConsoleApp3
{
    class Program
    {
        static void Main(string[] args)
        {
            int a = 1 + 1;
            Console.WriteLine(a);
        }
    }
}

これをILに変換してみます。

C#→ILに変換する専用のツールが必要かと思いきや、使うのはC#コンパイラ csc です。 cscを使うために Developer Command Prompt for VS 2017 を起動します。 Visual Studio をインストールしていれば名前は多少違うと思いますがどこかにあります。

起動した Developer Command Promp で上記のソースをコンパイルします。

> csc Program.cs

コンパイルが成功すると同じディレクトリにexeファイルが出来るはずです。 exeは実行形式ファイルとしておなじみですが、実はこの中にILが含まれています。 ただしバイトコード形式なので人間がそのまま読むのは(ほぼ)不可能です。

そこでildasmで逆アセンブルして人間が読める形に変換します。

>ildasm /output=Program.il Program.exe

成功すればProgram.ilというファイルが同じディレクトリに生成されます。

中身を見てみましょう(長いです)。

//  Microsoft (R) .NET Framework IL Disassembler.  Version 4.6.1055.0
//  Copyright (c) Microsoft Corporation.  All rights reserved.



// Metadata version: v4.0.30319
.assembly extern mscorlib
{
  .publickeytoken = (B7 7A 5C 56 19 34 E0 89 )                         // .z\V.4..
  .ver 4:0:0:0
}
.assembly Program
{
  .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilationRelaxationsAttribute::.ctor(int32) = ( 01 00 08 00 00 00 00 00 ) 
  .custom instance void [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.

  // --- The following custom attribute is added automatically, do not uncomment -------
  //  .custom instance void [mscorlib]System.Diagnostics.DebuggableAttribute::.ctor(valuetype [mscorlib]System.Diagnostics.DebuggableAttribute/DebuggingModes) = ( 01 00 07 01 00 00 00 00 ) 

  .hash algorithm 0x00008004
  .ver 0:0:0:0
}
.module Program.exe
// MVID: {63EE3CCC-2698-41E5-B6C1-F172BFAECB1D}
.imagebase 0x00400000
.file alignment 0x00000200
.stackreserve 0x00100000
.subsystem 0x0003       // WINDOWS_CUI
.corflags 0x00000001    //  ILONLY
// Image base: 0x04DF0000


// =============== CLASS MEMBERS DECLARATION ===================

.class private auto ansi beforefieldinit ConsoleApp3.Program
       extends [mscorlib]System.Object
{
  .method private hidebysig static void  Main(string[] args) cil managed
  {
    .entrypoint
    // Code size       11 (0xb)
    .maxstack  1
    .locals init (int32 V_0)
    IL_0000:  nop
    IL_0001:  ldc.i4.2
    IL_0002:  stloc.0
    IL_0003:  ldloc.0
    IL_0004:  call       void [mscorlib]System.Console::WriteLine(int32)
    IL_0009:  nop
    IL_000a:  ret
  } // end of method Program::Main

  .method public hidebysig specialname rtspecialname 
          instance void  .ctor() cil managed
  {
    // Code size       8 (0x8)
    .maxstack  8
    IL_0000:  ldarg.0
    IL_0001:  call       instance void [mscorlib]System.Object::.ctor()
    IL_0006:  nop
    IL_0007:  ret
  } // end of method Program::.ctor

} // end of class ConsoleApp3.Program


// =============================================================

// *********** DISASSEMBLY COMPLETE ***********************
// WARNING: Created Win32 resource file Program.res

メイン関数内で言うとたった2行だったコードがこんなに長くなるの?と 言いたくなりますが、他の様々な情報も含まれているのでメインの部分だけを抜き出します。

  .method private hidebysig static void  Main(string[] args) cil managed
  {
    .entrypoint
    // Code size       11 (0xb)
    .maxstack  1
    .locals init (int32 V_0)
    IL_0000:  nop
    IL_0001:  ldc.i4.2
    IL_0002:  stloc.0
    IL_0003:  ldloc.0
    IL_0004:  call       void [mscorlib]System.Console::WriteLine(int32)
    IL_0009:  nop
    IL_000a:  ret
  } // end of method Program::Main

なんとなくMainメソッドの面影が感じられるでしょうか。 nopldcなどアセンブリ命令っぽいものを見受けられます。 この中で登場する命令の意味を調べてまとめてみます。

命令 説明
nop 何もしない
ldc.i4.2 2をint32としてスタックにプッシュする
stloc.0 スタックから値を一つ「ローカル変数0」にポップする
ldloc.0 「ローカル変数0」の値をスタックに読み込む
call メソッドを呼び出す
ret メソッドからリターンする

List of CIL instructions - Wikipedia

命令の意味と合わせてILでのMain関数の処理内容を推測すると 「2をスタックにプッシュする」→「さっきプッシュした2をローカル変数0にポップする」→「さっき2を入れたローカル変数0の値をスタックに読み込む」→「Console.WriteLineメソッドを呼び出す」→「リターンする」となります。

2をプッシュしたりポップしたりするあたりが冗長な感じがしますが、 大したことはしてないことは分かると思います。ちなみに元のソースコードでは 1+1となっているところがいきなり2になっているのは恐らくコンパイラの 最適化が効いたためと思われます。

とりあえずここまで...

こんな感じで少しずつ IL に親しむシリーズをやりたいと思います。 めざせ IL マスター

C# の珍しいキーワード、using

C# を使っていて、滅多にお目にかからないキーワードが存在します。

前回の checked/unchecked、implicit/explicit, unsafe キーワードに引き続き、 今回は using キーワードのお話です。

maxfie1d.hatenablog.com maxfie1d.hatenablog.com maxfie1d.hatenablog.com

using には 2通りの使い方がある

usingキーワードには2つの使い方があります。

  1. using ディレクティブ (using System; みたいなの)
  2. using ステートメント (using (var resource = ...) { ... } みたいなの)

  3. の方は初心者でも馴染みがあると思いますが、 初めて 2. を見た時はusingってこういう使われ方もするの!?と驚くと思います。

というわけで 2. の方のusingを説明したいと思います。

using ステートメントの意義

C#GCがあるので不要になったものは自動的に後始末されますが、 ファイルやストリームなど使い終わったら即座に破棄するのが望ましいものもあります。

そういう即座に破棄するべきリソースはC#では共通してIDisposableインターフェースを実装するのが作法になっています。 IDisposableDisposeメソッドだけを持つインターフェースで、このDisposeメソッドを呼び出しさえすればリソースが 解放されるようにプログラムします。

namespace System
{
    //
    // Summary:
    //     Provides a mechanism for releasing unmanaged resources.
    //     マネージドでないリソースを解放する仕組みを提供します
    public interface IDisposable
    {
        //
        // Summary:
        //     Performs application-defined tasks associated with freeing, releasing, or resetting
        //     unmanaged resources.
        //     マネージドでないリソースを解放したりリセットすることに関する
        //     アプリケーション定義のタスクを行います
        void Dispose();
    }
}

IDisposableインターフェースを実装しているということは、逆に言えば 「使い終わったら必ず破棄してね」ということですが、破棄のし忘れというのはよくあることです。 また破棄のし忘れをIDEが警告を出してくれたらいいんですがそういうわけでもありません(おそらくチェックしてくれるツールはあります)。 また本来Disposeメソッドが呼ばれるようにしていたとしても途中で例外が発生して実は呼び出されなかったみたいなことも起こり得ります。 これの対策としてtry-catch-finallyで真面目に囲むとそれはそれで読みにくくもなります。

そこでusingステートメントの出番です。

using ステートメントの使い方

using (var resource = ...) { ごにょごにょする } が基本形です。 生成するインスタンスはもちろんIDisposableを実装していなければなりません。 ごにょごにょした後は自動的にリソースが解放されます。例外が発生しても解放されます。 usingさえ使えば安全にリソースの解放が行われます。

namespace ConsoleApp2
{
    class Disposable : IDisposable
    {
        public void Dispose() => throw new NotImplementedException();
    }

    class Program
    {
        static void Main(string[] args)
        {
            using (var d = new Disposable())
            {
                // ごにょごにょする
            }
        }
    }
}

ちょっと応用的な使い方

using ステートメント なので下のコードは正常なコードです。

if (cond)
    using (var d = new Disposable())
    {
        ...
    }

using (var d1 = new Disposable())
using (var d2 = new Disposable())
{
    ...
}

C# の珍しいキーワード、unsafe

C# を使っていて、滅多にお目にかからないキーワードが存在します。

前回の checked/unchecked、implicit/explicit キーワードに引き続き、 今回は unsafe キーワードのお話です。

maxfie1d.hatenablog.com

maxfie1d.hatenablog.com

unsafe とは

C# では実は C/C++ のポインタ操作のようなことが出来ます。 ただしこの時unsafeキーワードを使う必要があります。

unsafe の使い方の例

unsafeキーワードはメソッドの定義に加えると、 メソッド内でポインタ操作を行うことが出来るようになります。

また、unsafeなメソッドを呼び出すメソッドもunsafeを付ける必要があります。

注意: unsafe を含むコードをコンパイルするには/unsafeオプションを指定する必要があります。 ソリューションエクスプローラーのプロジェクトを右クリックしてプロパティを開き ビルド(Build) > アンセーフコードの許可(Allow unsafe code) にチェックを入れます。 f:id:maxfieldwalker:20180117121825p:plain

// See: https://docs.microsoft.com/ja-jp/dotnet/csharp/language-reference/keywords/unsafe
// /unsafeオプションでコンパイルします

class UnsafeTest
{
   // 安全でないメソッド: int へのポインタを引数に取ります
   unsafe static void SquarePtrParam(int* p)
   {
      *p *= *p;
   }

   unsafe static void Main()
   {
      int i = 5;
      // 安全でないメソッド: アドレス演算子を使います
      SquarePtrParam(&i);
      Console.WriteLine(i);
   }
}
// Output: 25

上記のコードを実行すると、25が出力されます。 変数i5が代入された後、SquarePtrParamメソッド内でポインタを介して値が書き換えられるというわけです。

また、unsafeはブロックにも使えます。 この場合ブロック内でのみポインタ操作が可能になります。

        static unsafe void Main(string[] args)
        {
            int i = 5;
            // unsafe ブロック
            unsafe
            {
                int* p = &i;
                *p *= *p;
            }
            Console.WriteLine(i);
            Console.ReadKey();
        }

C# の珍しいキーワード、implicit/explicit

C# を使っていて、滅多にお目にかからないキーワードが存在します。

前回の checked/unchecked キーワードに引き続き、 今回は implicit/explicit キーワードのお話です。

maxfie1d.hatenablog.com

implicit/explicit とは

C# では 「キャスト」が使えますが、暗黙的なキャストと明示的なキャストがあります。 implicitexplicitはキャストが行われる際にどのように値を変換するかを指定する場合に使います。

implicit の使い方の例

double 型の値をラップするだけのクラス Digit を定義します。 またdoubleDigit間を暗黙的に変換するメソッドを定義しています。

// See: https://docs.microsoft.com/ja-jp/dotnet/csharp/language-reference/keywords/implicit

class Digit
{
    public Digit(double d) { val = d; }
    public double val;

    public static implicit operator double(Digit d)
    {
        return d.val;
    }

    public static implicit operator Digit(double d)
    {
        return new Digit(d);
    }
}

こうすることでdoubleDigit間を暗黙的に変換することが 可能になります。

// See: https://docs.microsoft.com/ja-jp/dotnet/csharp/language-reference/keywords/implicit

class Program
{
    static void Main(string[] args)
    {
        Digit dig = new Digit(7);
        // これは 暗黙的な double オペレータを呼び出します
        //This call invokes the implicit "double" operator
        double num = dig;
        // これは 暗黙的な Digit オペレータを呼び出します
        Digit dig2 = 12;
        Console.WriteLine("num = {0} dig2 = {1}", num, dig2.val);
        Console.ReadLine();
    }
}

explicit の使い方の例

implicit と同様に型を変換するメソッドを定義してやります。

// See: https://docs.microsoft.com/ja-jp/dotnet/csharp/language-reference/keywords/explicit

class Fahrenheit
{
    public Fahrenheit(float temp)
    {
        degrees = temp;
    }
    // Fahrenheit から Celsius へ明示的に変換します
    public static explicit operator Celsius(Fahrenheit fahr)
    {
        return new Celsius((5.0f / 9.0f) * (fahr.degrees - 32));
    }
    public float Degrees
    {
        get { return degrees; }
    }
    private float degrees;
}

class Celsius
{
    public Celsius(float temp)
    {
        degrees = temp;
    }
    public float Degrees
    {
        get { return degrees; }
    }
    private float degrees;
}

ただしimplicitとは違いexplicit明示的にキャストする必要があります。

// See: https://docs.microsoft.com/ja-jp/dotnet/csharp/language-reference/keywords/explicit

class MainClass
{
    static void Main()
    {
        Fahrenheit fahr = new Fahrenheit(100.0f);
        Console.Write("{0} Fahrenheit", fahr.Degrees);
        Celsius c = (Celsius)fahr;

        Console.Write(" = {0} Celsius", c.Degrees);
    }
}

C# の珍しいキーワード、checked/unchecked

C# を使っていて、滅多にお目にかからないキーワードが存在します。

その1つである checked/unchecked キーワードのお話です。

checked/unchecked とは

オーバーフローチェックをするかしないかを明示する際に使います。 checkedで囲まれた部分はオーバーフローチェックを行い、 uncheckedで囲まれた部分はオーバーフローチェックされません。

checked/uncheckedキーワードは式と文に対して使用されます。

// checked を式に使う場合
int a = checked(1 + 2);

// checked を文に使う場合
checked
{
    int a = 1 + 2;
}

// checked を式に使う場合
int b = unchecked(1 + 2);

// checked を文に使う場合
unchecked
{
    int b = 1 + 2;
}

デフォルトの場合でオーバーフローチェックはされるか

checkeduncheckedも使わないデフォルトの場合では、 オーバーフローチェックは行われません。ちょっと意外です。 ただし、Visual Studio ではオーバーフローの恐れがある場合に警告が出るのでミスに気づくことができます。

C# には何故 let キーワードがないのか?

モダンな機能も取り入れつつ進化し続けているC#ですが、 イミュータブルの扱いがあまりよろしくありません。

Swift のletや、Kotlin のval に相当するものは C#にはないのでしょうか。

const ってあるじゃん

C/C++constとは違い、C#constコンパイル時に解決される値しか使えないのです。

readonly ってあるじゃん

クラスのフィールドなどには使えるのですが、ローカル変数には使えないのです。

C# で let キーワードは諦めるしかないのか...

実は数年前からローカル変数でのreadonlyは導入が検討されていて、 今にも入りそうなのですがまだ入っていない機能です。

github.com

提案にあるように、ローカルでreadonlyキーワードを使えるようにして さらにreadonly varの省略として新しくletvalキーワードを追加しようというものです。

// 全て同じ意味
readonly long maxBytesToDelete = (stream.LimitBytes - stream.MaxBytes) / 10;
readonly var maxBytesToDelete = (stream.LimitBytes - stream.MaxBytes) / 10;
let maxBytesToDelete = (stream.LimitBytes - stream.MaxBytes) / 10;
...
maxBytesToDelete = 0; // エラー: 宣言時以外での代入はできない

let キーワードは(おそらく)間もなく導入される

次の C# 8 では入ってくれ~(´;ω;`)

LINQ の実装を覗いてみる

ふと LINQ の拡張メソッドの実装を見てみたくなったので調べてみました。

LINQ のコードの入手

corefx というリポジトリで公開されています。

github.com

一番簡単そうな First() を見てみる。

IEnumerable の最初の要素を返す First() メソッドを見てます。

corefx/First.cs at master · dotnet/corefx · GitHub

        public static TSource First<TSource>(this IEnumerable<TSource> source)
        {
            TSource first = source.TryGetFirst(out bool found);
            if (!found)
            {
                throw Error.NoElements();
            }

            return first;
        }

TryGetFirst()メソッドの返り値を返しているようです。 ということでTryGetFirst()メソッドを見てみます。

        private static TSource TryGetFirst<TSource>(this IEnumerable<TSource> source, out bool found)
        {
            if (source == null)
            {
                throw Error.ArgumentNull(nameof(source));
            }

            if (source is IPartition<TSource> partition)
            {
                return partition.TryGetFirst(out found);
            }

            if (source is IList<TSource> list)
            {
                if (list.Count > 0)
                {
                    found = true;
                    return list[0];
                }
            }
            else
            {
                using (IEnumerator<TSource> e = source.GetEnumerator())
                {
                    if (e.MoveNext())
                    {
                        found = true;
                        return e.Current;
                    }
                }
            }

            found = false;
            return default(TSource);
        }

ちょっと長くなってきました。まずsourcenullでないかチェックしています。 次にsourceIPartitionなら~と分岐しています。ちょっと待った、IPartitionってなんだ。

IPartition インターフェースとは

ぐぐってみると参考になる記事がヒットしました。

azyobuzin.hatenablog.com

どうやらIPartitionインターフェースはIEnumerableであり、 さらにある程度LINQの処理を最適化できる場合に用いられるインターフェースのようです。

f:id:maxfieldwalker:20180112161220p:plain

話を戻します

というわけで、より高速に処理できるIPartitionならばそれがもつTryGetFirst()メソッドを 使おうというわけです。IPartitionでなければ次はIListなら~ と分岐します。これも高速化のためと思われます。

IPartitionでもIListでもなければ、諦めて最終手段です。コードの最後の部分を再掲します。 sourceGetEnumerator()を呼び出してIEnumeratorを取得し、一度だけMoveNext()を呼び出して、 要素があれば返しなければ失敗となります。

            else
            {
                using (IEnumerator<TSource> e = source.GetEnumerator())
                {
                    if (e.MoveNext())
                    {
                        found = true;
                        return e.Current;
                    }
                }
            }

            found = false;
            return default(TSource);

ほかにも色々

LINQには他にもまだまだメソッドがありますが、長くなりそうなのでこの辺りにしようと思います。 初めて内部実装を見るということをしましたが、結構工夫がなされていてここはこうなっていたんだ~とちょっと感動すらしました。 よい勉強になりました。