デリゲート生成
Action
やFunc
を動的に生成する際、条件によってオブジェクトが生成されたりされなかったり、自動でキャッシュされたりされなかったり、いろいろなことが起こります。
どうやって書くのがパフォーマンス的に優れているのかを整理するため、この記事では様々なパターンで内部的にどんなことが起こるのか、sharplab.ioを見ながら確認していきたいと思います。
検証内容はC# 10でのコンパイル結果に基づきます。
コンパイル結果に関係する条件は主に3点です。
- ①ラムダ式か、メソッド名の直接指定か
- ②(ラムダ式なら) ローカル変数をキャプチャするかどうか
- ③(ラムダ式なら) thisへの参照を持つか
各パターンを見ていく
①ラムダ式か、メソッド名の直接指定か
メソッド名を直接指定する場合
この場合、該当部分を呼び出す度にデリゲートが生成されます。
C# 11から、静的メソッドに限りメソッドグループからの生成でも自動でキャッシュが行われるように変更されたようです。
public class DelegateTest
{
public void SomeApi(Action callback){}
private void SomeMethod(){}
private static void SomeMethodStatic(){}
public void FromMethodGroup()
{
//毎回new Action()される
SomeApi(SomeMethod);
//毎回new Action()される (staticかどうかにかかわらない)
SomeApi(SomeMethodStatic);
}
}
デリゲートはヒープに確保されるオブジェクトであり、GCによる回収コストが発生します。
次のように手動でキャッシュする処理を入れることでオブジェクトの生成を抑えられます。
public class DelegateTest
{
public void SomeApi(Action callback){}
private void SomeMethod(){}
private Action manualCache = default;
public void FromMethodGroup()
{
//初回のみnewされる
if(manualCache == null) manualCache = SomeMethod;
SomeApi(manualCache);
}
}
② ローカル変数をキャプチャするかどうか
ラムダ式の中でローカル変数(引数を含む、メソッドのスコープで宣言された変数)をキャプチャすると、結構デカいコードに展開されます。
ローカル変数をキャプチャする場合
public class DelegateTest
{
public void SomeApi(Action callback){}
public void FromLambdaWithCaptureLocal()
{
int i = 1;
SomeApi(() => i++);
}
}
これは次のようなコードに展開されます。
public class DelegateTest
{
public void SomeApi(Action callback){}
public void FromLambdaWithCaptureLocal()
{
<>c__DisplayClass10_0 <>c__DisplayClass10_ = new <>c__DisplayClass10_0();
<>c__DisplayClass10_.i = 1;
SomeApi(new Action(<>c__DisplayClass10_.<FromLambdaWithCaptureLocal>b__0));
}
[CompilerGenerated]
private sealed class <>c__DisplayClass10_0
{
public int i;
internal void <FromLambdaWithCaptureLocal>b__0()
{
i++;
}
}
}
(コンパイラが自動的に生成しているコードのため、C#としては不正な書式になってます)
専用のクラスが定義され、呼び出しの度にインスタンスを生成しているのが分かります。グロい見た目をしてますね。
先程のようにキャッシュすることもできるかもしれませんが、それではキャプチャの意味がなくなる場合が多そうです。
キャプチャ対象をstaticなフィールドなどで代替できないか検討してみます。
(できない場合もあると思いますが、その時は諦めるか……もっと根本の設計を見直して対処できるかもしれません)
public class DelegateTest
{
public void SomeApi(Action callback){}
private static int tempField = 0;
public void FromLambdaWithCaptureLocal()
{
tempField = 1;
SomeApi(() => tempField++);
}
}
キャプチャしない場合
キャプチャしない場合は、専用クラスは生成されません。
次に関係してくるのは、this
への参照が必要かどうかです。
③ this
への参照を持つか
this
への参照が不要な場合
- ラムダ式の中で非
static
なメンバにアクセスしない場合
が該当します。
この場合、コンパイラが自動的にstatic
なキャッシュ用のフィールドを生成し、無駄なデリゲート生成を抑えてくれるようです。
public class DelegateTest
{
public void SomeApi(Action callback){}
private static void SomeMethodStatic(){}
public void FromLambdaNone()
{
//初回だけnew Action()され、2回目以降はキャッシュを使用する
SomeApi(() => {});
}
public void FromLambdaStatic()
{
//初回だけnew Action()され、2回目以降はキャッシュを使用する
SomeApi(() => SomeMethodStatic());
}
}
this
への参照が必要な場合
- ラムダ式の中で非
static
メンバにアクセスする場合
が該当します。
この場合、該当部分が呼び出されるたびにデリゲートのオブジェクトが生成されます。先程とは異なり自動的なキャッシュは行われません。
public class DelegateTest
{
public void SomeApi(Action callback){}
private void SomeMethod(){}
public void FromLambda()
{
//毎回new Action()される
SomeApi(() => SomeMethod());
}
}
メソッド名の直接指定の際と同様、手動でキャッシュする処理を入れることでオブジェクトの生成を抑えられます。
public class DelegateTest
{
public void SomeApi(Action callback){}
private void SomeMethod(){}
private Action manualCache = default;
public void FromLambda()
{
//初回のみnewされる
if(manualCache == null) manualCache = () => SomeMethod();
SomeApi(manualCache);
}
}
各パターンをまとめる
まとめるとこんな感じです。
-
メソッド名からの生成
- 呼び出し毎にnewされる
- 手動でキャッシュするのがよい
-
ラムダ式からの生成
-
ローカル変数をキャプチャする場合
- 専用クラスが生成され、呼び出し毎にnewされる
- キャプチャを回避できないか検討する
-
ローカル変数をキャプチャしない場合
-
this
への参照が必要な場合- 呼び出し毎にnewされる
- 手動でキャッシュするのがよい
-
this
への参照が不要な場合- 自動的にキャッシュされ、初回だけnewされる
-
-
ローカル変数をキャプチャする場合
ある程度網羅したつもりですが、なにか漏れがあれば指摘してくださると幸いです。