C#7で Local Functions を使用する場合、パラメーター(または他のローカル変数)をメインメソッドからローカル関数に渡すには、2つのオプションがあります。パラメーターを明示的に宣言することもできます。他の関数、または単純に包含メソッドからパラメータ/変数を「キャプチャ」して直接使用することができます。
例はおそらくこれを最もよく示しています:
明示的に宣言する
public int MultiplyFoo(int id)
{
return LocalBar(id);
int LocalBar(int number)
{
return number * 2;
}
}
キャプチャ
public int MultiplyFoo(int id)
{
return LocalBar();
int LocalBar()
{
return id * 2;
}
}
どちらの方法も同じように機能しますが、ローカル関数を呼び出す方法が異なります。
だから私の質問は:
注意すべき2つの違いはありますか?パフォーマンス、メモリ割り当て、ガベージコレクション、メンテナンス性などについて考えています。
C#のローカル関数は、少なくともRoslynの実装では、キャプチャの点で優れています。コンパイラーがローカル関数からデリゲートを作成していないことを保証できる場合(または変数の寿命を延ばすために何か他のことをしている場合)は、キャプチャーされたすべての変数とともにref
パラメーターを使用できますローカル関数と通信するために生成された構造体。たとえば、2番目のメソッドは次のようになります。
public int MultiplyFoo(int id)
{
__MultiplyFoo__Variables variables = new __MultiplyFoo__Variables();
variables.id = id;
return __Generated__LocalBar(ref variables);
}
private struct __MultiplyFoo__Variables
{
public int id;
}
private int __Generated__LocalBar(ref __MultiplyFoo__Variables variables)
{
return variables.id * 2;
}
したがって、(たとえば)ラムダ式がデリゲートに変換される場合のように、ヒープの割り当ては必要ありません。一方、構造体が作成され、そこに値がコピーされます。 int
を値で渡すのが、構造体を参照で渡すよりも効率がよいかどうかはnlikelyが重要です...ただし、巨大な構造体がローカル変数の場合、暗黙的なキャプチャを使用する方が単純な値パラメーターを使用するよりも効率的です。 (ローカル関数がキャプチャされたローカル変数を大量に使用した場合も同様です。)
さまざまなローカル関数によって複数のローカル変数がキャプチャされている場合、状況はさらに複雑になります。さらに、それらの一部がループ内のローカル関数である場合などはさらに複雑になります。ildasm
やReflectorなどを使用した探索は非常に楽しいものです。
非同期メソッド、イテレータブロック、ローカル関数内のラムダ式の記述、メソッドグループ変換を使用したローカル関数からのデリゲートの作成など、複雑なことを始めるとすぐに、その時点で推測を続けるのはためらいます。コードをそれぞれの方法でベンチマークするか、ILを確認するか、より単純なコードを記述して、より大きなパフォーマンス検証テスト(すでに持っているでしょう?)に依存して、問題があるかどうかを知らせます。 。
興味深い質問でした。まず、ビルド出力を逆コンパイルしました。
public int MultiplyFoo(int id)
{
return LocalFunctionTests.\u003CMultiplyFoo\u003Eg__LocalBar\u007C0_0(id);
}
public int MultiplyBar(int id)
{
LocalFunctionTests.\u003C\u003Ec__DisplayClass1_0 cDisplayClass10;
cDisplayClass10.id = id;
return LocalFunctionTests.\u003CMultiplyBar\u003Eg__LocalBar\u007C1_0(ref cDisplayClass10);
}
Idをパラメーターとして渡すと、渡されたidパラメーターを使用してローカル関数が呼び出されます。特別なことは何もありません。パラメータはメソッドのスタックフレームに格納されます。ただし、パラメーターを渡さない場合は、フィールド(cDisplayClass10.id = id)を持つ構造体(Daisyが指摘したように「クラス」という名前が付けられています)が作成され、IDがそれに割り当てられます。次に、構造体がローカル関数に参照として渡されます。 C#コンパイラはクロージャをサポートするためにそれをしているようです。
パフォーマンスに関しては、Stopwatch.ElapsedTicksを使用し、パラメーターとしてIDを渡す方が一貫して高速でした。フィールドを持つ構造体を作成するコストが原因だと思います。ローカル関数に別のパラメーターを追加すると、パフォーマンスのギャップが拡大しました。
これは私のテストコードです、誰かが興味があれば
public int MultiplyFoo(int id, int id2)
{
return LocalBar(id, id2);
int LocalBar(int number, int number2)
{
return number * number2 * 2;
}
}
public int MultiplyBar(int id, int id2)
{
return LocalBar();
int LocalBar()
{
return id * id2 * 2;
}
}
[Fact]
public void By_Passing_Id()
{
var sut = new LocalFunctions();
var watch = Stopwatch.StartNew();
for (int i = 0; i < 10000; i++)
{
sut.MultiplyFoo(i, i);
}
_output.WriteLine($"Elapsed: {watch.ElapsedTicks}");
}
[Fact]
public void By_Not_Passing_Id()
{
var sut = new LocalFunctions();
var watch = Stopwatch.StartNew();
for (int i = 0; i < 10000; i++)
{
sut.MultiplyBar(i, i);
}
_output.WriteLine($"Elapsed: {watch.ElapsedTicks}");
}