コンパイルされたC#ラムダ式のパフォーマンス


91

コレクションに対する次の簡単な操作を検討してください。

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);

次に、式を使用します。次のコードはほぼ同等です。

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

そしてデバッグをオフにして:

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

デバッグオン、最適化オン:

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

デバッグオフ、最適化オン:

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

新しいサプライズ。VS2008(C#3)からVS2010(C#4)に切り替えるとUsingLambdaCombined、ネイティブラムダよりも高速になります。


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

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

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


3
これらのタイミングはVisual Studioで実行されたものですか?その場合は、リリースモードのビルドを使用してタイミングを繰り返し、デバッグせずに実行します(つまり、Visual StudioのCtrl + F5またはコマンドラインから)。また、Stopwatchではなくタイミングに使用することを検討してくださいDateTime.Now
ジムミッシェル

12
なぜ遅いのかわかりませんが、ベンチマーク手法はあまり良くありません。まず、DateTime.Nowの精度は1/64秒にすぎないため、測定の丸め誤差が大きくなります。代わりにストップウォッチを使用してください。正確には数ナノ秒です。次に、コードをjitする時間(最初の呼び出し)とそれ以降のすべての呼び出しの両方を測定します。それは平均を落とすことができます。(この場合、10万のMAXはjitの負担を平均化するのにおそらく十分ですが、それでも平均に含めることは悪い習慣です。)
Eric Lippert

7
@Eric、丸めエラーは、各操作でDateTime.Now.Ticksが使用され、開始前と終了後のミリ秒カウントがパフォーマンスの違いを示すのに十分な場合にのみ発生します。
Akash Kava

1
ストップウォッチを使用している場合は、この記事に従って正確な結果を保証することをお勧めします:codeproject.com/KB/testing/stopwatch-measure-precise.aspx
Zach Green

1
@エリック、私はそれが利用可能な最も正確な測定技術ではないことに同意しますが、私たちは違いの大きさについて話しています。MAXは、大きな偏差を減らすのに十分な高さです。
Hugo Sereno Ferreira、

回答:


43

内側のラムダがコンパイルされていない可能性がありますか?概念実証は次のとおりです。

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の第一人者に、内部で起こっていることについて少しコメントしてもらいたいと思います。

そして、なぜなぜこれがネイティブのラムダよりも速いのです


1
私は自分の回答を受け入れることを考えています。なぜなら、それが投票数が最も多い回答だからです。もう少し待つべきですか?
Hugo Sereno Ferreira、

ネイティブラムダよりも速くコードを取得する場合に何が起こるかについて、マイクロベンチマーク(実際にはJava固有のものは何もありませんが、名前はあります)に関するこのページを参照することをお勧めします:code.google.com/p/caliper/wiki / JavaMicrobenchmarks
Blaisorblade、

動的にコンパイルされたラムダの方が速い理由については、最初に実行される「ラムダの使用」は、コードをJITする必要があることで不利になっているのではないかと思います。
Oskar Berggren

何が起こっているのかわかりません。一度、フィールドとプロパティの設定と取得のためにコンパイルされた式とcreatedelegateをテストすると、プロパティの方がcreatedelegateの方がはるかに高速でしたが、フィールドの方がわずかに高速でした
nawfal

10

最近私はほとんど同じ質問をしました:

コンパイルしてデリゲート式のパフォーマンス

私のためのソリューションは、私が呼び出してはならないことだったCompileExpression、私が呼び出す必要があることCompileToMethod、それにしてコンパイルExpressionするstatic動的アセンブリ内のメソッド。

そのようです:

var assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(
  new AssemblyName("MyAssembly_" + Guid.NewGuid().ToString("N")), 
  AssemblyBuilderAccess.Run);

var moduleBuilder = assemblyBuilder.DefineDynamicModule("Module");

var typeBuilder = moduleBuilder.DefineType("MyType_" + Guid.NewGuid().ToString("N"), 
  TypeAttributes.Public));

var methodBuilder = typeBuilder.DefineMethod("MyMethod", 
  MethodAttributes.Public | MethodAttributes.Static);

expression.CompileToMethod(methodBuilder);

var resultingType = typeBuilder.CreateType();

var function = Delegate.CreateDelegate(expression.Type,
  resultingType.GetMethod("MyMethod"));

しかし、それは理想的ではありません。私はこれが正確に適用されるタイプにはかなり確かではないんだけど、私は、デリゲートによってパラメータとして取られ、またはデリゲートによって返されるタイプはと思う持っていることpublicと、非ジェネリック。ジェネリック型System.__Canonは、ジェネリック型の内部で.NETによって使用される内部型にアクセスするようであり、これは「ジェネリック型である必要がある」に違反するため、非ジェネリックでなければなりませんpublic

これらのタイプでは、明らかに遅いものを使用できますCompile。次の方法でそれらを検出します。

private static bool IsPublicType(Type t)
{

  if ((!t.IsPublic && !t.IsNestedPublic) || t.IsGenericType)
  {
    return false;
  }

  int lastIndex = t.FullName.LastIndexOf('+');

  if (lastIndex > 0)
  {
    var containgTypeName = t.FullName.Substring(0, lastIndex);

    var containingType = Type.GetType(containgTypeName + "," + t.Assembly);

    if (containingType != null)
    {
      return containingType.IsPublic;
    }

    return false;
  }
  else
  {
    return t.IsPublic;
  }
}

しかし、私が言ったように、これは理想的ではなく、メソッドを動的アセンブリにコンパイルする方が時々桁違いに速い理由を知りたいです。と私は時々言います。なぜなら、Expressionコンパイルされたwith Compileが通常のメソッドと同じくらい速い場合もあるからです。そのための私の質問を参照してください。

あるいは、誰かがpublic動的アセンブリで「非型なし」制約を回避する方法を知っている場合も、それは歓迎されます。


4

式は同等ではないため、結果は歪んでいます。これをテストするためのテストベンチを作成しました。テストには、通常のラムダ呼び出し、同等のコンパイルされた式、手作りの同等のコンパイルされた式、および構成されたバージョンが含まれます。これらはより正確な数値である必要があります。興味深いことに、プレーンバージョンと構成バージョンの間に大きな違いは見られません。そして、コンパイルされた式は自然に遅くなりますが、ほんの少しだけです。適切な数値を取得するには、十分な大きさの入力と反復回数が必要です。それは違いを生みます。

2番目の質問については、これからどのようにしてより多くのパフォーマンスを引き出すことができるかわからないので、私はあなたを助けることはできません。それは手に入るのと同じくらい良く見えます。

HandMadeLambdaExpression()メソッドの3番目の質問に対する私の答えが見つかります。拡張メソッドのために構築するのが最も簡単な式ではありませんが、実行可能です。

using System;
using System.Collections.Generic;
using System.Linq;

using System.Diagnostics;
using System.Linq.Expressions;

namespace ExpressionBench
{
    class Program
    {
        static void Main(string[] args)
        {
            var values = Enumerable.Range(0, 5000);
            var lambda = GetLambda();
            var lambdaExpression = GetLambdaExpression().Compile();
            var handMadeLambdaExpression = GetHandMadeLambdaExpression().Compile();
            var composed = GetComposed();
            var composedExpression = GetComposedExpression().Compile();
            var handMadeComposedExpression = GetHandMadeComposedExpression().Compile();

            DoTest("Lambda", values, lambda);
            DoTest("Lambda Expression", values, lambdaExpression);
            DoTest("Hand Made Lambda Expression", values, handMadeLambdaExpression);
            Console.WriteLine();
            DoTest("Composed", values, composed);
            DoTest("Composed Expression", values, composedExpression);
            DoTest("Hand Made Composed Expression", values, handMadeComposedExpression);
        }

        static void DoTest<TInput, TOutput>(string name, TInput sequence, Func<TInput, TOutput> operation, int count = 1000000)
        {
            for (int _ = 0; _ < 1000; _++)
                operation(sequence);
            var sw = Stopwatch.StartNew();
            for (int _ = 0; _ < count; _++)
                operation(sequence);
            sw.Stop();
            Console.WriteLine("{0}:", name);
            Console.WriteLine("  Elapsed: {0,10} {1,10} (ms)", sw.ElapsedTicks, sw.ElapsedMilliseconds);
            Console.WriteLine("  Average: {0,10} {1,10} (ms)", decimal.Divide(sw.ElapsedTicks, count), decimal.Divide(sw.ElapsedMilliseconds, count));
        }

        static Func<IEnumerable<int>, IList<int>> GetLambda()
        {
            return v => v.Where(i => i % 2 == 0).Where(i => i > 5).ToList();
        }

        static Expression<Func<IEnumerable<int>, IList<int>>> GetLambdaExpression()
        {
            return v => v.Where(i => i % 2 == 0).Where(i => i > 5).ToList();
        }

        static Expression<Func<IEnumerable<int>, IList<int>>> GetHandMadeLambdaExpression()
        {
            var enumerableMethods = typeof(Enumerable).GetMethods();
            var whereMethod = enumerableMethods
                .Where(m => m.Name == "Where")
                .Select(m => m.MakeGenericMethod(typeof(int)))
                .Where(m => m.GetParameters()[1].ParameterType == typeof(Func<int, bool>))
                .Single();
            var toListMethod = enumerableMethods
                .Where(m => m.Name == "ToList")
                .Select(m => m.MakeGenericMethod(typeof(int)))
                .Single();

            // helpers to create the static method call expressions
            Func<Expression, ParameterExpression, Func<ParameterExpression, Expression>, Expression> WhereExpression =
                (instance, param, body) => Expression.Call(whereMethod, instance, Expression.Lambda(body(param), param));
            Func<Expression, Expression> ToListExpression =
                instance => Expression.Call(toListMethod, instance);

            //return v => v.Where(i => i % 2 == 0).Where(i => i > 5).ToList();
            var exprParam = Expression.Parameter(typeof(IEnumerable<int>), "v");
            var expr0 = WhereExpression(exprParam,
                Expression.Parameter(typeof(int), "i"),
                i => Expression.Equal(Expression.Modulo(i, Expression.Constant(2)), Expression.Constant(0)));
            var expr1 = WhereExpression(expr0,
                Expression.Parameter(typeof(int), "i"),
                i => Expression.GreaterThan(i, Expression.Constant(5)));
            var exprBody = ToListExpression(expr1);
            return Expression.Lambda<Func<IEnumerable<int>, IList<int>>>(exprBody, exprParam);
        }

        static Func<IEnumerable<int>, IList<int>> GetComposed()
        {
            Func<IEnumerable<int>, IEnumerable<int>> composed0 =
                v => v.Where(i => i % 2 == 0);
            Func<IEnumerable<int>, IEnumerable<int>> composed1 =
                v => v.Where(i => i > 5);
            Func<IEnumerable<int>, IList<int>> composed2 =
                v => v.ToList();
            return v => composed2(composed1(composed0(v)));
        }

        static Expression<Func<IEnumerable<int>, IList<int>>> GetComposedExpression()
        {
            Expression<Func<IEnumerable<int>, IEnumerable<int>>> composed0 =
                v => v.Where(i => i % 2 == 0);
            Expression<Func<IEnumerable<int>, IEnumerable<int>>> composed1 =
                v => v.Where(i => i > 5);
            Expression<Func<IEnumerable<int>, IList<int>>> composed2 =
                v => v.ToList();
            var exprParam = Expression.Parameter(typeof(IEnumerable<int>), "v");
            var exprBody = Expression.Invoke(composed2, Expression.Invoke(composed1, Expression.Invoke(composed0, exprParam)));
            return Expression.Lambda<Func<IEnumerable<int>, IList<int>>>(exprBody, exprParam);
        }

        static Expression<Func<IEnumerable<int>, IList<int>>> GetHandMadeComposedExpression()
        {
            var enumerableMethods = typeof(Enumerable).GetMethods();
            var whereMethod = enumerableMethods
                .Where(m => m.Name == "Where")
                .Select(m => m.MakeGenericMethod(typeof(int)))
                .Where(m => m.GetParameters()[1].ParameterType == typeof(Func<int, bool>))
                .Single();
            var toListMethod = enumerableMethods
                .Where(m => m.Name == "ToList")
                .Select(m => m.MakeGenericMethod(typeof(int)))
                .Single();

            Func<ParameterExpression, Func<ParameterExpression, Expression>, Expression> LambdaExpression =
                (param, body) => Expression.Lambda(body(param), param);
            Func<Expression, ParameterExpression, Func<ParameterExpression, Expression>, Expression> WhereExpression =
                (instance, param, body) => Expression.Call(whereMethod, instance, Expression.Lambda(body(param), param));
            Func<Expression, Expression> ToListExpression =
                instance => Expression.Call(toListMethod, instance);

            var composed0 = LambdaExpression(Expression.Parameter(typeof(IEnumerable<int>), "v"),
                v => WhereExpression(
                    v,
                    Expression.Parameter(typeof(int), "i"),
                    i => Expression.Equal(Expression.Modulo(i, Expression.Constant(2)), Expression.Constant(0))));
            var composed1 = LambdaExpression(Expression.Parameter(typeof(IEnumerable<int>), "v"),
                v => WhereExpression(
                    v,
                    Expression.Parameter(typeof(int), "i"),
                    i => Expression.GreaterThan(i, Expression.Constant(5))));
            var composed2 = LambdaExpression(Expression.Parameter(typeof(IEnumerable<int>), "v"),
                v => ToListExpression(v));

            var exprParam = Expression.Parameter(typeof(IEnumerable<int>), "v");
            var exprBody = Expression.Invoke(composed2, Expression.Invoke(composed1, Expression.Invoke(composed0, exprParam)));
            return Expression.Lambda<Func<IEnumerable<int>, IList<int>>>(exprBody, exprParam);
        }
    }
}

そして私のマシン上の結果:

ラムダ:
  経過:340971948 123230(ミリ秒)
  平均:340.971948 0.12323(ミリ秒)
ラムダ式:
  経過:357077202 129051(ミリ秒)
  平均:357.077202 0.129051(ミリ秒)
手作りラムダ式:
  経過:345029281 124696(ミリ秒)
  平均:345.029281 0.124696(ミリ秒)

構成:
  経過:340409238 123027(ミリ秒)
  平均:340.409238 0.123027(ms)
構成された表現:
  経過:350800599 126782(ミリ秒)
  平均:350.800599 0.126782(ミリ秒)
手作りの作曲表現:
  経過:352811359 127509(ミリ秒)
  平均:352.811359 0.127509(ミリ秒)

3

デリゲートを介したコンパイルされたラムダパフォーマンスは、実行時にコンパイルされたコードが最適化されない可能性があるために遅くなる可能性がありますが、手動で記述したコードとC#コンパイラを介してコンパイルされたコードは最適化されます。

第2に、複数のラムダ式は複数の無名メソッドを意味し、それらのそれぞれを呼び出すことは、まっすぐなメソッドを評価するよりも少し時間がかかります。たとえば、

Console.WriteLine(x);

そして

Action x => Console.WriteLine(x);
x(); // this means two different calls..

コンパイラは、2つの異なる呼び出しを実行するため、2つ目のオーバーヘッドが必要です。最初にx自体を呼び出し、次にその呼び出しxのステートメント内で呼び出します。

したがって、組み合わせたラムダは確かに単一のラムダ式よりもパフォーマンスがほとんど低下しません。

そして、正しいロジックを評価しているが、コンパイラーが実行する追加のステップを追加しているため、これは内部で実行されているものとは無関係です。

式ツリーがコンパイルされた後でも、最適化は行われず、小さな複雑な構造が保持されます。評価して呼び出すと、追加の検証やnullチェックなどが行われ、コンパイルされたラムダ式のパフォーマンスが低下する可能性があります。


2
よく見ると、UsingLambdaCombinedテストは複数のラムダ関数を組み合わせており、そのパフォーマンスはに非常に近い UsingLambdaです。最適化に関しては、それらはJITエンジンによって処理されるため、ランタイムで生成されたコード(コンパイル後)もJIT最適化のターゲットになると確信しました。
ヒューゴセレノフェレイラ

1
JIT最適化とコンパイル時最適化は、プロジェクト設定でコンパイル時最適化をオフにできる2つの異なるものです。第二に、式のコンパイルはおそらく動的なMSILを出力しますが、ロジックと操作のシーケンスには必要に応じてnullチェックと有効性が含まれるため、やはり少し遅くなります。それがどのようにコンパイルされているかについてリフレクターを調べることができます。
Akash Kava

2
あなたの推論は正しいですが、私はこの特定の問題についてあなたに同意しなければなりません(つまり、大きさの違いは静的コンパイルによるものではありません)。1つ目は、コンパイル時の最適化を実際に無効にした場合でも、違いはかなり大きいためです。次に、動的生成をわずかに遅くするように最適化する方法をすでに見つけたためです。「なぜ」を理解してみて、結果を掲載します。
Hugo Sereno Ferreira
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.