【C#】デリゲートの引数は呼び方でパフォーマンスが違う

引数がデリゲートのメソッドは呼び出し方がいくつかあります。書き方でコンパイラが展開する方法が異なるります。このため一部実行コストやパフォーマンスにも差が出るようなのでまとめてみました。

確認環境

この記事は以下環境で確認しています。

  • VisualStudio 2019
  • .NET 5(C#9.0)
  • SharpLabでコード確認

呼び出し方の種類

まず、例えば、以下のメソッドがあったとします。

// Actionデリゲート(戻り値も引数もないデリゲート)を引数に取るメソッド
public static void Foo(Action act)
{
    act();
}

次にこのメソッドを呼び出す方法はだいたい以下の4通りがあります。

public static void Sample()
{
    Init();

    // ★(1) デリゲートをnewして呼び出す
    Foo(new Action(OnAction));

    // ★(2) メソッド名だけ指定する
    Foo(OnAction);

    // ★(3) ラムダ式にして呼び出す
    Foo(() => OnAction());
    Foo(() => Console.WriteLine("called"));

    // ★(4) 定義済みのデリゲートを呼び出す
    Foo(_act1); // 4-1
    Foo(_act2); // 4-2
    Foo(_act3); // 4-2
}

// -x-x- 以下、上記処理に必要な実装 -x-x-
private static Action _act1 = () => { Console.WriteLine("called"); };
private static Action _act2;
private static Action _act3;

private static void Init()
{
    _act2 = new Action(OnAction);
    _act3 = () => { Console.WriteLine("called"); };
}

private static void OnAction()
{
    Console.WriteLine("called");
}

public static void Foo(Action act)
{
    act();
}

展開のされ方

上記のコード中のコメントの通りですが、以下4通りの展開のされ方を各々見ていきたいと思います。

  • (1) デリゲートをnewして呼び出す
  • (2) メソッド名だけ指定する
  • (3) ラムダ式にして呼び出す
  • (4) 定義済みのデリゲートを呼び出す

(1)と(2)の展開のされかた

(1) と (2) は結果が同じになります。毎回デリゲートをnewしてデリゲートを作成してメソッドに渡されています。

// ★(1) デリゲートをnewして呼び出す
Foo(new Action(OnAction));
// ★(2) メソッド名だけ指定する
Foo(OnAction);

// ↓↓↓↓

// 同じように展開される
Foo(new Action(OnAction));
Foo(new Action(OnAction));

(3)の展開のされかた

(3) のラムダ式はコンパイラーがかなりインテリジェンスに展開してくれます。

以下少し読みにくいですが展開されたコードをそのまま載せています。少し読み取りにくいですが、初回に new して以降は使いまわしてるようです。メソッドを直接指定と違って毎回 new しませんが、存在チェックが呼び出し毎に入ります。

// (3) ラムダ式にして呼び出す
Foo(() => OnAction());
Foo(() => Console.WriteLine("called"));

// ↓↓↓↓

// 以下のようにいい感じに自動生成される
[Serializable]
[CompilerGenerated]
private sealed class <>c
{
    public static readonly <>c <>9 = new <>c();

    public static Action <>9__1_0;

    internal void <Sample>b__1_0()
    {
        OnAction();
    }

    internal void <Sample>b__1_1()
    {
        Console.WriteLine("called");
    }
}

Foo(<>c.<>9__1_0 ?? (<>c.<>9__1_0 = new Action(<>c.<>9.<Sample>b__1_0)));
Foo(<>c.<>9__1_1 ?? (<>c.<>9__1_1 = new Action(<>c.<>9.<Sample>b__1_1)));

(4)の展開のされかた

(4) はメソッド呼び出しはコード変わらないですが、呼び出し方によって自動生成されたりされなかったりします。ラムダ式を途中で使うと自動生成されるコードが出現します。従って "4-1", "4-3" のような書き方は無駄以外の何物でもないのでやめましょう。最速を狙うなら "4-2" 以外ありません。

// ★(4) 定義済みのデリゲートを呼び出す
Foo(_act1); // 4-1
Foo(_act2); // 4-2
Foo(_act3); // 4-2

private static Action _act1 = () => { Console.WriteLine("called"); };
private static Action _act2;
private static Action _act3;

private static void Init()
{
    _act2 = new Action(OnAction);
    _act3 = () => { Console.WriteLine("called"); };
}

private static void OnAction()
{
    Console.WriteLine("called");
}

// ↓↓↓↓

Foo(_act1); // ここは変わらない
Foo(_act2);
Foo(_act3);

// 以下の自動生成がされる

[Serializable]
[CompilerGenerated]
private sealed class <>c
{
    public static readonly <>c <>9 = new <>c();

    public static Action <>9__5_0;

    internal void <Init>b__5_0()
    {
        Console.WriteLine("called");
    }

    internal void <.cctor>b__9_0()
    {
        Console.WriteLine("called");
    }
}

private static Action _act1 = new Action(<>c.<>9.<.cctor>b__8_0);
private static Action _act2;

private static void Init()
{
    _act2 = new Action(OnAction);
    _act3 = <>c.<>9__5_0 ?? (<>c.<>9__5_0 = new Action(<>c.<>9.<Init>b__5_0));
}

結論

結論は以下の通りです。

  • (a) 引数にメソッド名を指定するのは効率が悪い(=呼び出し毎に new される)
  • (b) ラムダ式はいい感じにしてくれる
  • (c) 一番効率がいいのは"4-2"だけど実装が結構面倒

(a) のメソッド名をデリゲートに直接指定するのは毎回デリゲートを new するため高頻度な実行個所では使わないほうがいいです。Unity のフレーム毎に呼び出される Update メソッドや高頻度で実行される個所にこの書き方を使うと破棄されたデリゲートの GC でスパイクの原因になりやすそうです。

(b) は基本的にメソッドをラムダ式で包んでおけば、あとは「いい感じ」にしてくれます。通常この書き方問題ないです。ベストチョイスです。

(c) はコンパイラーの自動生成を抑止しつつチェックもしないので最速で実行できますが実装はかなり面倒です。(b) と呼び出しの時に null チェックする or しないかだけの差しかないので、このチェック有無は最適化の最後でやっと対象になるかどうか程度のためパフォーマンスを狙って最初からこの実装にする必要は一切無いと思います。

// まとめ

// この呼び出し方はNG
Foo(OnAction);


// いい感じにしてくれるのでラムダを積極的に使う
Foo(() => OnAction());
Foo(() => Console.WriteLine("called"));


// 一番効率が良いが実装が面倒(可読性も落ちる)
Foo(_act2);

private static Action _act2;
private static void Init()
{
    _act2 = new Action(OnAction);
}
private static void OnAction()
{
    Console.WriteLine("called");
}