developer tip

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

optionbox 2020. 9. 4. 07:11
반응형

컴파일 된 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)

Compiled lambda performance over delegates may be slower because Compiled code at runtime may not be optimized however the code you wrote manually and that compiled via C# compiler is optimized.

Second, multiple lambda expressions means multiple anonymous methods, and calling each of them takes little extra time over evaluating a straight method. For example, calling

Console.WriteLine(x);

and

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

are different, and with second one little more overhead is required as from compiler's perspective, its actually two different calls. First calling x itself and then within that calling x's statement.

So your combined Lambda will certainly have little slow performance over single lambda expression.

And this is independent of what is executing inside, because you are still evaluating correct logic, but you are adding additional steps for compiler to perform.

Even after expression tree is compiled, it will not have optimization, and it will still preserve its little complex structure, evaluating and calling it may have extra validation, null check etc which might be slowing down performance of compiled lambda expressions.

참고URL : https://stackoverflow.com/questions/5568294/compiled-c-sharp-lambda-expressions-performance

반응형