web-dev-qa-db-ja.com

C#のローカル関数-パラメータを渡すときにキャプチャするかしないか?

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つの違いはありますか?パフォーマンス、メモリ割り当て、ガベージコレクション、メンテナンス性などについて考えています。

30
Dan Diplo

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を確認するか、より単純なコードを記述して、より大きなパフォーマンス検証テスト(すでに持っているでしょう?)に依存して、問題があるかどうかを知らせます。 。

14
Jon Skeet

興味深い質問でした。まず、ビルド出力を逆コンパイルしました。

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を渡す方が一貫して高速でした。フィールドを持つ構造体を作成するコストが原因だと思います。ローカル関数に別のパラメーターを追加すると、パフォーマンスのギャップが拡大しました。

  • 合格ID:2247
  • Idを渡さない:2566

これは私のテストコードです、誰かが興味があれば

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}");
}
5
Andy