7
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

C#で クロージャ のパフォーマンスを測定してみる

Last updated at Posted at 2019-09-18

概要

.NET (C#) のパフォーマンス改善に関する記事にて、クロージャを利用した場合 と そうでない場合 における
パフォーマンスの差異を指摘するような情報を拝見しました。
普段、コードを書く際には配慮しないような事項だったため、勉強の意味も込めて実際に測定してみました。

情報整理

匿名関数でローカル変数をキャプチャした (取り込んだ) 場合、その関数を クロージャ と呼びます。

C#においてクロージャを作成した場合、コンパイル後に生成されるコード (IL = 中間言語) にて、
クロージャが取り込んだ ローカル変数 を保持するようなクラス構造が、暗黙的に生成されます。

クロージャの外部 (呼び出し側) でローカル変数が変更された場合でも、
クロージャの内部でローカル変数の変更が共有出来るのは、このような仕組みによるものです。

下記ページに非常に判りやすい解説があります。
https://ufcpp.net/study/csharp/sp2_anonymousmethod.html#static

環境

  • Windows10
  • Visual Studio 2019
  • .NET Core 2.2
  • BenchmarkDotNet

コードサンプル

サンプルコードを Github にアップしています。
https://github.com/tYoshiyuki/dotnet-core-benchmark-test

尚、今回使用したプログラムサンプルは、下記スライド (41スライド目) を参考にさせていただきました。
https://www.slideshare.net/xin9le/dotnetperformancetips-170268354

解説

Linq の FirstOrDefault を例として、ローカル変数をキャプチャした場合 と そうでない場合 とでベンチマークを測定してみます。

ポイントとなるコードは以下の通りです。

SampleRepository.cs

private readonly List<Sample> _samples = new List<Sample>
{
    new Sample{ Id = 1, Name = "One"},
    new Sample{ Id = 2, Name = "Two"},
    new Sample{ Id = 3, Name = "Three"}
};

public Sample GetByLambda(int id)
{
    // 普通にラムダ式で呼び出し、変数のキャプチャ有り
    return _samples.FirstOrDefault(_ => _.Id == id);
}

public Sample GetByExtension(int id)
{
    // 拡張メソッドを利用、変数のキャプチャ無し
    return _samples.FirstOrDefault((x, state) => x.Id == state, id);
}

public Sample GetByLocalFunction(int id)
{
    // ローカル関数経由で呼び出し、変数のキャプチャ有り
    bool Func(Sample s) => s.Id == id;
    return _samples.FirstOrDefault(Func);
}

変数をキャプチャするパターンを2つ、拡張メソッド経由で変数をキャプチャしないパターンを1つ実装しました。
実際のRepositoryでは、DBのようなデータストアからデータを取得しますが、今回はメモリ上より取得するようにしています。

呼び出し側のコードを下記の通りです。

BenchmarkMain.cs

public BenchmarkMain()
{
    _sampleRepository = new SampleRepository();
    _indexes = Enumerable.Range(0, 10000).ToArray();
}

[Benchmark]
public void CaseLambda()
{
    // 普通にラムダ式で呼び出し、変数のキャプチャ有り
    foreach (var i in _indexes)
    {
        var id = i % 3 + 1;
        var sample = _sampleRepository.GetByLambda(id);
        if (sample == null) Console.WriteLine("No data");
    }
}

・・・以下略

それぞれのパターンで 10,000回 の呼び出しを行っています。
ベンチマークの測定には BenchmarkDotNet を利用しました。

以下、測定結果を掲載します。

Method Mean Error StdDev Median
CaseLambda 1,234.8 us 35.82 us 101.04 us 1,198.6 us
CaseExtension 782.1 us 23.95 us 69.86 us 771.0 us
CaseLocalFunction 1,401.2 us 38.64 us 113.32 us 1,371.5 us

やはり、ローカル変数をキャプチャしない方が パフォーマンス的に有利 であるようです。
実践では、コードの可読性やパフォーマンスをどこまで配慮すべきかという観点もあるとは思いますが、
実装上のポイントとして、認識しておいた方が良いかなと感じました。

おまけ

暗黙的に生成されたクラスを IL で確認してみました。
素人には読めるような内容ではないのですが、キャプチャされたローカル変数を保持するクラス構造が
生成されているという点は確認出来ます。

  .class nested private sealed auto ansi beforefieldinit
    '<>c__DisplayClass1_0'
      extends [System.Runtime]System.Object
  {
    .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
      = (01 00 00 00 )

    .field public int32 id // ★★★ この部分 ★★★

    .method public hidebysig specialname rtspecialname instance void
      .ctor() cil managed
    {
      .maxstack 8

      IL_0000: ldarg.0      // this
      IL_0001: call         instance void [System.Runtime]System.Object::.ctor()
      IL_0006: nop
      IL_0007: ret

    } // end of method '<>c__DisplayClass1_0'::.ctor

    .method assembly hidebysig instance bool
      '<GetByLambda>b__0'(
        class DotNetCoreBenchmarkTest.Repository.Sample _
      ) cil managed
    {
      .maxstack 8

      // [19 49 - 19 59]
      IL_0000: ldarg.1      // _
      IL_0001: callvirt     instance int32 DotNetCoreBenchmarkTest.Repository.Sample::get_Id()
      IL_0006: ldarg.0      // this
      IL_0007: ldfld        int32 DotNetCoreBenchmarkTest.Repository.SampleRepository/'<>c__DisplayClass1_0'::id
      IL_000c: ceq
      IL_000e: ret

    } // end of method '<>c__DisplayClass1_0'::'<GetByLambda>b__0'
  } // end of class '<>c__DisplayClass1_0'
7
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?