編譯C#Lambda表達式性能

c# expression-trees lambda performance

考慮以下對集合的簡單操作:

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,調試ON,優化OFF:

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比原生lambda更快。


好吧,我找到了一種方法來將lambda編譯的性能提高一個數量級以上。這是一個提示;運行探查器後,92%的時間用於:

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

嗯...為什麼在每次迭代中創建一個新的委託?我不確定,但解決方案是在一個單獨的帖子中。

一般承認的答案

可能是內部的lambda沒有被編譯?!?這是一個概念證明:

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

活泉!它不僅速度快,而且比原生lambda快。 ( 從頭開始 )。


當然上面的代碼太難以編寫了。讓我們做一些簡單的魔術:

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,Optimizations ON,Debugging OFF:

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

現在你可以爭辯說我沒有動態生成整個表達式;只是鏈接調用。但在上面的例子中,我生成了整個表達式。時間匹配。這只是編寫更少代碼的捷徑。


根據我的理解,正在發生的是.Compile()方法不會將編譯傳播到內部lambdas,因此不會將CreateDelegate的常量調用傳播。但要真正理解這一點,我希望有一個.NET大師對內部事情的評論。

為什麼為什麼現在比現在的lambda更快!?


熱門答案

最近我問了一個幾乎相同的問題:

編譯到委託表達式的性能

我的解決方案是我不應該在Expression上調用Compile ,但我應該在其上調用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;
  }
}

但就像我說的那樣,這並不理想,我仍然想知道為什麼編譯動態裝配的方法有時會快一個數量級。我有時會說,因為我也看到過使用CompileExpression和普通方法一樣快的情況。看到我的問題。

或者如果有人知道繞過動態程序集的“無非public類型”約束的方法,那也是受歡迎的。



許可下: CC-BY-SA with attribution
不隸屬於 Stack Overflow
這個KB合法嗎? 是的,了解原因
許可下: CC-BY-SA with attribution
不隸屬於 Stack Overflow
這個KB合法嗎? 是的,了解原因