🌟

【C#】クラスとクロージャーのパフォーマンスの違いを比較してみた

2023/01/25に公開約2,800字

前書き

クラスとクロージャーでどれほど処理速度やメモリ使用量などに差が出るか気になったのでそれの比較を行った。その結果をここにまとめることとした。
いかに今回使用したコードがまとまっている。
https://github.com/ystsbry/experiment/tree/main/ComparisonOfClosureAndClassSpeeds

実験用プログラムの実装

実験用プログラムは10回インクリメントしてその値を返すプログラムとし、それの速度やヒープをどれだけ確保したかを計測することとした。

まずはクラスの実装から。いたってシンプルです。特にいうことはないでしょう。

public class ComparisionTargetClass
{
    public int Count = 0;

    public void Increment()
    {
        Count++;
    }
}

次にクロージャーの実装です。ローカル変数を定義したら、それをキャプチャする関数を二つ返すようにした。ひとつはインクリメントする関数。もう一つはローカル変数の値を取り出す関数です。

public class ComparisionTargetClosure
{
    static public (Action, Func<int>) Get()
    {
        int count = 0;
        return (
        () => 
        {
            count++;
        },
        () =>
        {
            return count;
        });
    }
}

次に最初に書いた要件を満たすコードを先ほどのクラスとクロージャーそれぞれで実装した。今回の速度の計測ではBenchmarkDotnetを利用した。上の実装でクラスの計測を行い、下の実装でクロージャーの計測を行った。

[MemoryDiagnoser]
public class BenchmarkTest
{

    [Benchmark]
    public int ClassBenchmark()
    {
        var TargetClass = new ComparisionTargetClass();

        for (var i = 0; i < 10; i++)
        {
            TargetClass.Increment();
        }
        return TargetClass.Count;
    }

    [Benchmark]
    public int ClosureBenchmark()
    {
        var (IncrementClosure, GetCountClosure) = ComparisionTargetClosure.Get();

        for (var i = 0; i < 10; i++)
        {
            IncrementClosure();
        }
        return GetCountClosure();
    }
}

Mainはこのようになっている。

internal class Program
{
    private static void Main() => BenchmarkRunner.Run<BenchmarkTest>();
}

使用している名前空間の一覧は以下のようになっている。

using BenchmarkDotNet.Running;
using BenchmarkDotNet.Attributes;

計測結果

dotnet run --configuration Releaseとコマンドをたたくと計測結果が出てきた。以下にその計測結果を示す。

|           Method |      Mean |     Error |    StdDev |   Gen0 | Allocated |
|----------------- |----------:|----------:|----------:|-------:|----------:|
|   ClassBenchmark |  7.614 ns | 0.0497 ns | 0.0465 ns | 0.0029 |      24 B |
| ClosureBenchmark | 32.894 ns | 0.4465 ns | 0.3958 ns | 0.0181 |     152 B |

この表の見方に関しては以下のようになっている。

  • method : 計測したメソッドの名前
  • Mean : 平均
  • Error : 誤差
  • StdDev : 標準偏差
  • Allocated : 確保したヒープの量

処理にかけた平均時間においても、確保したヒープの量においてもClassの実装のほうが速く、かつ軽量であることを示した。すべてを知っているわけではないのでもしかするとクロージャのほうが速く、軽量であるケースもあるかもしれないが探すのは骨が折れそうなのでここで留めておくこととする。

最後に

クラスのほうが速かったからとは言え、クロージャーも適切に使えば可読性や使い手にとって使いやすい関数を提供できる便利な機能であると思う。(c#のsystem.threading.channelsはおそらくクロージャーを活用してProducer Consumerパターンを再現していると思われる。)
また、最初はActionだったりFunc<>だったり正直使いずらいなと思っていて、TypeScriptと作者が同じなら(Type1: type) => Type2こんな感じに関数の型を定義させてくれてもいいじゃんなどと思っていたが、この計測をした今ではむしろこのデザインでよかったなと思っている。型の書き方が直観的じゃないので敬遠していたがなんとなく狙い通りに書かされた気分だけどまあいっか。

参考文献

https://github.com/dotnet/BenchmarkDotNet/
https://qiita.com/Tokeiya/items/30d8a76163622a4b5be1
https://cactuaroid.hatenablog.com/entry/2018/10/23/212729

Discussion

ログインするとコメントできます