[c#] 컴파일 된 C # Lambda 표현식 성능

컬렉션에 대한 다음과 같은 간단한 조작을 고려하십시오.

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네이티브 람다보다 빠릅니다.


좋아, 나는 람다 컴파일 성능을 10 배 이상 향상시킬 수있는 방법을 찾았다. 팁 이요; 프로파일 러를 실행 한 후 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 전문가가 내부 작업에 대해 약간의 의견을 말하고 싶습니다.

그리고 , 오 이것이 이제 네이티브 람다보다 더 빠릅니다!?


답변

최근에 거의 동일한 질문을했습니다.

대리자로 컴파일 된 식의 성능

나를 위해이 솔루션은 내가 전화를 안이었다 CompileExpression,하지만 난 전화를해야 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- type”제약 조건 을 우회하는 방법을 알고있는 사람도 환영합니다.


답변

식이 동일하지 않으므로 왜곡 된 결과가 나타납니다. 나는 이것을 테스트하기 위해 테스트 벤치를 썼다. 테스트에는 정규 람다 호출, 동등한 컴파일 된 표현식, 손으로 만든 동등한 컴파일 된 표현식 및 구성된 버전이 포함됩니다. 더 정확한 숫자 여야합니다. 흥미롭게도, 나는 평범한 버전과 작곡 된 버전 사이에 많은 차이를 보지 못하고 있습니다. 그리고 컴파일 된 표현식은 자연스럽게 느리지 만 아주 조금만 느립니다. 좋은 숫자를 얻으려면 충분한 입력 및 반복 횟수가 필요합니다. 차이를 만듭니다.

두 번째 질문에 대해서는 어떻게하면 더 많은 성능을 얻을 수 있을지 모르기 때문에 거기에서 도움을 드릴 수 없습니다. 그것은 얻을 것만 큼 좋아 보인다.

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 :
  경과 : 340971948 123230 (ms)
  평균 : 340.971948 0.12323 (ms)
람다 식 :
  경과 : 357077202 129051 (ms)
  평균 : 357.077202 0.129051 (ms)
손으로 만든 Lambda 표현 :
  경과 : 345029281 124696 (ms)
  평균 : 345.029281 0.124696 (ms)

구성 :
  경과 : 340409238 123027 (ms)
  평균 : 340.409238 0.123027 (ms)
구성된 표현 :
  경과 : 350800599 126782 (ms)
  평균 : 350.800599 0.126782 (ms)
손으로 만든 구성된 표현 :
  경과 : 352811359 127509 (ms)
  평균 : 352.811359 0.127509 (ms)


답변

런타임에 컴파일 된 코드는 최적화되지 않을 수 있지만 수동으로 작성하고 C # 컴파일러를 통해 컴파일 된 코드는 최적화되어 있기 때문에 대리자를 통해 컴파일 된 람다 성능이 느려질 수 있습니다.

둘째, 여러 람다 식은 여러 익명 메서드를 의미하며 각 메서드를 호출하면 직선 메서드를 평가하는 것보다 약간의 추가 시간이 걸립니다. 예를 들어

Console.WriteLine(x);

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

컴파일러의 관점에서 볼 때 두 번째로 약간 더 많은 오버 헤드가 필요하며 실제로 두 번의 다른 호출이 필요합니다. 먼저 x 자체를 호출 한 다음 해당 호출 x의 문 내에서 호출합니다.

따라서 결합 된 Lambda는 단일 람다 표현식에 비해 성능이 거의 저하되지 않습니다.

그리고 이것은 올바른 논리를 평가하고 있지만 컴파일러가 수행 할 추가 단계를 추가하고 있기 때문에 내부에서 실행되는 것과는 독립적입니다.

표현식 트리가 컴파일 된 후에도 최적화되지 않고 여전히 약간 복잡한 구조를 유지하며 평가하고 호출하면 컴파일 된 람다 표현식의 성능을 저하시킬 수있는 추가 유효성 검사, 널 검사 등이있을 수 있습니다.


답변