Зібрано продуктивність виразів лямбда-виразів на 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швидше, ніж рідна лямбда.


Гаразд, я знайшов спосіб покращити ефективність компіляції лямбда більш ніж на порядок. Ось порада; після запуску профайлера 92% часу витрачається на:

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

Хм-м-м-м ... Чому це створює нового делегата на кожній ітерації? Я не впевнений, але рішення йде в окремому дописі.


3
Чи ці терміни запускаються у Visual Studio? Якщо так, повторіть синхронізацію, використовуючи режим випуску та запуск без налагодження (тобто Ctrl + F5 у Visual Studio або з командного рядка). Крім того, розгляньте можливість використання Stopwatchчасу, а не DateTime.Now.
Джим Мішель,

12
Я не знаю, чому це повільніше, але ваша техніка порівняння не дуже хороша. По-перше, DateTime.Now має точність лише до 1/64 секунди, тому ваша похибка округлення вимірювань велика. Замість цього використовуйте секундомір; він точний до декількох наносекунд. По-друге, ви вимірюєте як час на введення коду (перший дзвінок), так і кожен наступний дзвінок; що може скинути середні значення. (Хоча в цьому випадку МАКСУ сотні тисяч, мабуть, достатньо, щоб узагальнити навантаження на джит, все ж погано застосовувати його до середнього.)
Ерік Ліпперт,

7
@Eric, помилка округлення може бути лише в тому випадку, якщо в кожній операції використовується DateTime.Now.Ticks, перед початком і після закінчення кількість мілісекунд досить висока, щоб показати різницю в продуктивності.
Akash Kava

1
якщо використовується секундомір, я рекомендую слідувати цій статті, щоб забезпечити точні результати: codeproject.com/KB/testing/stopwatch-measure-precise.aspx
Зак Грін,

1
@Eric, хоча я згоден, що це не найточніша доступна техніка вимірювання, ми говоримо про порядок різниці. MAX є достатньо високим, щоб зменшити значні відхилення.
Уго Серено Феррейра

Відповіді:


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

Woot! Це не тільки швидко, це швидше, ніж рідна лямбда. ( Почухати голову ).


Звичайно, наведений вище код просто надто болісний для написання. Давайте зробимо просту магію:

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
Я думаю прийняти власну відповідь, оскільки саме вона набрала найбільше голосів. Чи слід почекати ще трохи?
Уго Серено Феррейра

Про те, що відбувається з тим, що ви отримуєте код швидше, ніж рідна лямбда, можливо, ви захочете поглянути на цю сторінку про мікровимірювальні позначки (яка не має нічого конкретно для Java, незважаючи на назву): code.google.com/p/caliper/wiki / JavaMicrobenchmarks
Blaisorblade

Що стосується того, чому динамічно скомпільована лямбда-швидкість швидша, я підозрюю, що "використання лямбда-сигналу", яке запускається першим, карається необхідністю JIT-коду.
Оскар Берггрен

Я не знаю, що відбувається, одного разу, коли я тестував скомпільований вираз і створював elegate для встановлення та отримання з полів та властивостей, createdelegate був набагато швидшим для властивостей, але компілювався був дуже трохи швидшим для полів
nawfal

10

Нещодавно я задав майже ідентичне запитання:

Виконання компільованого для делегування виразу

Рішенням для мене було те, що я не повинен закликати Compileдо Expression, але що я повинен зателефонувати CompileToMethodдо нього та скомпілювати Expressiona до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обмеження "no non- types" за допомогою динамічної збірки, це також вітається.


4

Ваші вирази не еквівалентні, і, таким чином, ви отримуєте нерівні результати. Я написав тестовий стенд, щоб перевірити це. Тести включають звичайний лямбда-виклик, еквівалентний скомпільований вираз, ручний еквівалентний скомпільований вираз, а також складені версії. Це повинні бути більш точні цифри. Цікаво, що я не бачу великих розбіжностей між звичайною та складеною версіями. І скомпільовані вирази повільніші, природно, але дуже мало. Вам потрібен досить великий вхід і кількість ітерацій, щоб отримати якісні цифри. Це робить різницю.

Що стосується Вашого другого питання, я не знаю, як Ви змогли б отримати від цього більше результатів, тому я не можу Вам там допомогти. Це виглядає так само добре, як і збирається отримати.

Ви знайдете мою відповідь на ваше третє запитання в 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);
        }
    }
}

І результати на моїй машині:

Лямбда:
  Минуло: 340971948 123230 (мс)
  Середнє: 340,971948 0,12323 (мс)
Лямбда-вираз:
  Минуло: 357077202 129051 (мс)
  Середнє: 357.077202 0,129051 (мс)
Виготовлений вручну лямбда-вираз:
  Минуло: 345029281 124696 (мс)
  Середнє: 345,029281 0,124696 (мс)

Складено:
  Минуло: 340409238 123027 (мс)
  Середнє: 340.409238 0,123027 (мс)
Складений вираз:
  Минуло: 350800599 126782 (мс)
  Середнє: 350.800599 0,126782 (мс)
Складений вираз ручної роботи:
  Минуло: 352811359 127509 (мс)
  Середнє: 352,811359 0,127509 (мс)

3

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

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

Console.WriteLine(x);

і

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

різні, і з другим потрібно трохи більше накладних витрат, з точки зору компілятора, насправді це два різні виклики. Спочатку виклик самого x, а потім усередині оператора виклику x.

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

І це не залежить від того, що виконується всередині, оскільки ви все ще оцінюєте правильну логіку, але додаєте додаткові кроки для виконання компілятора.

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


2
Якщо придивитися, UsingLambdaCombinedтест поєднує в собі кілька лямбда-функцій, і його ефективність дуже близька до UsingLambda. Щодо оптимізацій, я був переконаний, що ними оброблявся механізм JIT, і, отже, код, згенерований під час виконання (після компіляції), також буде ціллю будь-якої оптимізації JIT.
Hugo Sereno Ferreira

1
Оптимізація JIT та оптимізація часу компіляції - це дві різні речі, які можна відключити в налаштуваннях проекту. По-друге, компіляція виразів, ймовірно, буде випромінювати динамічний MSIL, що знову ж буде трохи повільнішим, оскільки логіка та послідовність операцій буде містити нульові перевірки та обґрунтованість відповідно до потреб. Ви можете подивитися в рефлекторі щодо його складання.
Акаш Кава,

2
Хоча ваші міркування обґрунтовані, я повинен не погодитися з вами щодо цієї конкретної проблеми (тобто, різниця в порядку величини не обумовлена ​​статичною компіляцією). По-перше, оскільки, якщо ви насправді вимкнете оптимізацію часу компіляції, різниця все ще значна. По-друге, тому, що я вже знайшов спосіб оптимізувати динамічне покоління, щоб бути лише трохи повільнішим. Дозвольте мені спробувати зрозуміти "чому", і я опублікую результати.
Hugo Sereno Ferreira
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.