Производительность Скомпилированных Лямбда-Выражений C#


рассмотрим следующие простые манипуляции над коллекцией:

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, отладки На:

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 быстрее, чем родной лямбды.


хорошо, я нашел способ улучшить скомпилированную лямбду производительности более чем на порядок. Вот совет; после запуска профилировщика 92% времени тратится на:

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

хмммм... Почему он создает новый делегат в каждой итерации? Я не уверен, но решение следует в отдельном посте.

4 89

4 ответа:

может быть, внутренние лямбды не компилируются?!? Вот доказательство концепции:

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 прокомментировал немного о внутренних вещах, происходящих.

и почему, о почему это теперь быстрее, чем родной лямда!?

недавно я задал почти идентичный вопрос:

производительность скомпилированного выражения для делегирования

решение для меня было то, что я не должен называть Compile на Expression, но это я должен назвать 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 составлен с Compile так же быстро, как обычный метод. См. мой вопрос для этого.

или если кто-то знает способ обойти "нет номера-public типы" ограничение с динамической сборкой, это тоже приветствуется.

ваши выражения не эквивалентны, и таким образом вы получите перекос результаты. Я написал тестовый стенд, чтобы проверить это. Тесты включают в себя обычный лямбда-вызов, эквивалентное скомпилированное выражение, ручное эквивалентное скомпилированное выражение, а также составленные версии. Это должны быть более точные цифры. Интересно, что я не вижу большого различия между простыми и составленными версиями. И скомпилированные выражения медленнее, естественно, но только очень мало. Нужен достаточно большой вход и количество итераций, чтобы получить некоторые хорошие цифры. Это имеет значение.

Что касается вашего второго вопроса, я не знаю, как вы сможете получить больше производительности из этого, поэтому я не могу вам помочь. Это выглядит так хорошо, как он собирается получить.

вы найдете мой ответ на ваш третий вопрос HandMadeLambdaExpression() метод. Не самое простое выражение для построения из-за методов расширения, но выполнимо.

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

и результаты на моей машине:

Lambda:
  Elapsed:  340971948     123230 (ms)
  Average: 340.971948    0.12323 (ms)
Lambda Expression:
  Elapsed:  357077202     129051 (ms)
  Average: 357.077202   0.129051 (ms)
Hand Made Lambda Expression:
  Elapsed:  345029281     124696 (ms)
  Average: 345.029281   0.124696 (ms)

Composed:
  Elapsed:  340409238     123027 (ms)
  Average: 340.409238   0.123027 (ms)
Composed Expression:
  Elapsed:  350800599     126782 (ms)
  Average: 350.800599   0.126782 (ms)
Hand Made Composed Expression:
  Elapsed:  352811359     127509 (ms)
  Average: 352.811359   0.127509 (ms)

скомпилированная лямбда-производительность по делегатам может быть медленнее, потому что скомпилированный код во время выполнения не может быть оптимизирован, однако код, который вы написали вручную, и который скомпилирован с помощью компилятора C#, оптимизирован.

во-вторых, несколько лямбда-выражений означает несколько анонимных методов, и вызов каждого из них занимает немного дополнительного времени по сравнению с оценкой прямого метода. Например, вызов

Console.WriteLine(x);

и

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

отличаются, а со вторым мало больше накладных расходов требуется, поскольку с точки зрения компилятора, на самом деле это два разных вызова. Во-первых, называющая себя х, а затем в том, что назвав заявление "х".

таким образом, ваша объединенная лямбда, безусловно, будет иметь небольшую медленную производительность по сравнению с одним лямбда-выражением.

и это не зависит от того, что выполняется внутри, потому что вы все еще оцениваете правильную логику, но вы добавляете дополнительные шаги для выполнения компилятора.

даже после того, как дерево выражения скомпилирован, он не будет иметь оптимизации, и он все равно сохранит свою небольшую сложную структуру, оценивая и вызывая ее, может иметь дополнительную проверку, нулевую проверку и т. д., которые могут замедлять производительность скомпилированных лямбда-выражений.