1. ホーム
  2. c#

[解決済み] コンパイルされたC#ラムダ式のパフォーマンス

2023-03-13 19:21:20

質問

コレクションに対する次のような簡単な操作を考えてみましょう。

static List<int> x = new List<int>() { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
var result = x.Where(i => i % 2 == 0).Where(i => i > 5);

では、Expressionsを使ってみましょう。次のようなコードがおおよそ相当します。

static void UsingLambda() {
    Func<IEnumerable<int>, IEnumerable<int>> lambda = l => l.Where(i => i % 2 == 0).Where(i => i > 5);
    var t0 = DateTime.Now.Ticks;
    for (int j = 1; j < MAX; j++) 
        var sss = lambda(x).ToList();

    var tn = DateTime.Now.Ticks;
    Console.WriteLine("Using lambda: {0}", tn - t0);
}

しかし、オンザフライで式を構築したいので、新しいテストを紹介します。

static void UsingCompiledExpression() {
    var f1 = (Expression<Func<IEnumerable<int>, IEnumerable<int>>>)(l => l.Where(i => i % 2 == 0));
    var f2 = (Expression<Func<IEnumerable<int>, IEnumerable<int>>>)(l => l.Where(i => i > 5));
    var argX = Expression.Parameter(typeof(IEnumerable<int>), "x");
    var f3 = Expression.Invoke(f2, Expression.Invoke(f1, argX));
    var f = Expression.Lambda<Func<IEnumerable<int>, IEnumerable<int>>>(f3, argX);

    var c3 = f.Compile();

    var t0 = DateTime.Now.Ticks;
    for (int j = 1; j < MAX; j++) 
        var sss = c3(x).ToList();

    var tn = DateTime.Now.Ticks;
    Console.WriteLine("Using lambda compiled: {0}", tn - t0);
}

もちろん上記と全く同じというわけではないので、公平を期すため、最初のものを少し修正します。

static void UsingLambdaCombined() {
    Func<IEnumerable<int>, IEnumerable<int>> f1 = l => l.Where(i => i % 2 == 0);
    Func<IEnumerable<int>, IEnumerable<int>> f2 = l => l.Where(i => i > 5);
    Func<IEnumerable<int>, IEnumerable<int>> lambdaCombined = l => f2(f1(l));
    var t0 = DateTime.Now.Ticks;
    for (int j = 1; j < MAX; j++) 
        var sss = lambdaCombined(x).ToList();

    var tn = DateTime.Now.Ticks;
    Console.WriteLine("Using lambda combined: {0}", tn - t0);
}

MAX = 100000、VS2008、デバッグONの場合の結果です。

Using lambda compiled: 23437500
Using lambda:           1250000
Using lambda combined:  1406250

そして、デバッグOFFの状態で

Using lambda compiled: 21718750
Using lambda:            937500
Using lambda combined:  1093750

サプライズ . コンパイルされた式は、他の選択肢に比べておよそ17倍も遅いのです。さて、ここからが問題です。

  1. 等価でない式を比較しているのか。
  2. コンパイルした式を .NET に "最適化" させる機構はありますか?
  3. 同じチェーンコールをどのように表現すればよいですか。 l.Where(i => i % 2 == 0).Where(i => i > 5); をプログラム的に表現するには?

さらにいくつかの統計。Visual Studio 2010 で、デバッグをオン、最適化をオフにしました。

Using lambda:           1093974
Using lambda compiled: 15315636
Using lambda combined:   781410

デバッギングON、最適化ON。

Using lambda:            781305
Using lambda compiled: 15469839
Using lambda combined:   468783

デバッグはOFF、最適化はON。

Using lambda:            625020
Using lambda compiled: 14687970
Using lambda combined:   468765

新しい驚き。 VS2008(C#3)からVS2010(C#4)に切り替えたことによって UsingLambdaCombined がネイティブのラムダより速くなりました。


OK、ラムダコンパイルのパフォーマンスを1桁以上向上させる方法を見つけました。これはヒントです。プロファイラーを実行した後、92% の時間が費やされます。

System.Reflection.Emit.DynamicMethod.CreateDelegate(class System.Type, object)

うーん... なぜ繰り返しごとに新しいデリゲートを作成しているのでしょうか?よくわかりませんが、解決策は別記事で続きます。

どのように解決するのか?

内側のラムダがコンパイルされていない可能性があります! 以下はその証明です。

static void UsingCompiledExpressionWithMethodCall() {
        var where = typeof(Enumerable).GetMember("Where").First() as System.Reflection.MethodInfo;
        where = where.MakeGenericMethod(typeof(int));
        var l = Expression.Parameter(typeof(IEnumerable<int>), "l");
        var arg0 = Expression.Parameter(typeof(int), "i");
        var lambda0 = Expression.Lambda<Func<int, bool>>(
            Expression.Equal(Expression.Modulo(arg0, Expression.Constant(2)),
                             Expression.Constant(0)), arg0).Compile();
        var c1 = Expression.Call(where, l, Expression.Constant(lambda0));
        var arg1 = Expression.Parameter(typeof(int), "i");
        var lambda1 = Expression.Lambda<Func<int, bool>>(Expression.GreaterThan(arg1, Expression.Constant(5)), arg1).Compile();
        var c2 = Expression.Call(where, c1, Expression.Constant(lambda1));

        var f = Expression.Lambda<Func<IEnumerable<int>, IEnumerable<int>>>(c2, l);

        var c3 = f.Compile();

        var t0 = DateTime.Now.Ticks;
        for (int j = 1; j < MAX; j++)
        {
            var sss = c3(x).ToList();
        }

        var tn = DateTime.Now.Ticks;
        Console.WriteLine("Using lambda compiled with MethodCall: {0}", tn - t0);
    }

そして、今のタイミングは

Using lambda:                            625020
Using lambda compiled:                 14687970
Using lambda combined:                   468765
Using lambda compiled with MethodCall:   468765

うぉぉぉぉぉぉぉぉぉぉぉぉぉぉぉぉぉぉぉぉぉぉぉぉぉ 速いだけでなく、ネイティブのラムダより速いです。( 頭を掻く ).


もちろん、上記のコードは単に書くのが面倒なだけです。簡単な魔法をかけましょう。

static void UsingCompiledConstantExpressions() {
    var f1 = (Func<IEnumerable<int>, IEnumerable<int>>)(l => l.Where(i => i % 2 == 0));
    var f2 = (Func<IEnumerable<int>, IEnumerable<int>>)(l => l.Where(i => i > 5));
    var argX = Expression.Parameter(typeof(IEnumerable<int>), "x");
    var f3 = Expression.Invoke(Expression.Constant(f2), Expression.Invoke(Expression.Constant(f1), argX));
    var f = Expression.Lambda<Func<IEnumerable<int>, IEnumerable<int>>>(f3, argX);

    var c3 = f.Compile();

    var t0 = DateTime.Now.Ticks;
    for (int j = 1; j < MAX; j++) {
        var sss = c3(x).ToList();
    }

    var tn = DateTime.Now.Ticks;
    Console.WriteLine("Using lambda compiled constant: {0}", tn - t0);
}

VS2010, 最適化オン, デバッグオフの場合です。

Using lambda:                            781260
Using lambda compiled:                 14687970
Using lambda combined:                   468756
Using lambda compiled with MethodCall:   468756
Using lambda compiled constant:          468756

さて、式全体を動的に生成しているわけではなく、呼び出しの連鎖だけだと主張することもできます。しかし、上記の例では、式全体を生成しています。そして、タイミングも一致しています。これは、より少ないコードを書くためのショートカットなのです。


私の理解では、何が起こっているかというと、.Compile()メソッドがコンパイルを内部ラムダに伝搬しないため、定数呼び出しの際に CreateDelegate . しかし、これを本当に理解するために、私は.NETの第一人者に、起こっている内部のものについて少しコメントしてもらいたいと思っています。

そして なぜ を、ああ なぜ はネイティブのラムダより速くなったのか!?