동적 변수는 성능에 어떤 영향을 미칩니 까?


128

dynamicC # 의 성능에 대한 질문이 있습니다 . 읽은 dynamic결과 컴파일러가 다시 실행되지만 어떤 역할을합니까?

dynamic매개 변수로 사용 된 변수를 사용 하여 전체 메서드를 다시 컴파일해야합니까? 아니면 동적 동작 / 컨텍스트가있는 줄만 다시 컴파일해야합니까 ?

dynamic변수 를 사용 하면 간단한 for 루프가 2 배 정도 느려질 수 있다는 것을 알았습니다 .

내가 사용한 코드 :

internal class Sum2
{
    public int intSum;
}

internal class Sum
{
    public dynamic DynSum;
    public int intSum;
}

class Program
{
    private const int ITERATIONS = 1000000;

    static void Main(string[] args)
    {
        var stopwatch = new Stopwatch();
        dynamic param = new Object();
        DynamicSum(stopwatch);
        SumInt(stopwatch);
        SumInt(stopwatch, param);
        Sum(stopwatch);

        DynamicSum(stopwatch);
        SumInt(stopwatch);
        SumInt(stopwatch, param);
        Sum(stopwatch);

        Console.ReadKey();
    }

    private static void Sum(Stopwatch stopwatch)
    {
        var sum = 0;
        stopwatch.Reset();
        stopwatch.Start();
        for (int i = 0; i < ITERATIONS; i++)
        {
            sum += i;
        }
        stopwatch.Stop();

        Console.WriteLine(string.Format("Elapsed {0}", stopwatch.ElapsedMilliseconds));
    }

    private static void SumInt(Stopwatch stopwatch)
    {
        var sum = new Sum();
        stopwatch.Reset();
        stopwatch.Start();
        for (int i = 0; i < ITERATIONS; i++)
        {
            sum.intSum += i;
        }
        stopwatch.Stop();

        Console.WriteLine(string.Format("Class Sum int Elapsed {0}", stopwatch.ElapsedMilliseconds));
    }

    private static void SumInt(Stopwatch stopwatch, dynamic param)
    {
        var sum = new Sum2();
        stopwatch.Reset();
        stopwatch.Start();
        for (int i = 0; i < ITERATIONS; i++)
        {
            sum.intSum += i;
        }
        stopwatch.Stop();

        Console.WriteLine(string.Format("Class Sum int Elapsed {0} {1}", stopwatch.ElapsedMilliseconds, param.GetType()));
    }

    private static void DynamicSum(Stopwatch stopwatch)
    {
        var sum = new Sum();
        stopwatch.Reset();
        stopwatch.Start();
        for (int i = 0; i < ITERATIONS; i++)
        {
            sum.DynSum += i;
        }
        stopwatch.Stop();

        Console.WriteLine(String.Format("Dynamic Sum Elapsed {0}", stopwatch.ElapsedMilliseconds));
    }

아니요, 컴파일러를 실행하지 않으므로 첫 번째 패스에서 속도가 느려집니다. Reflection과 다소 비슷하지만 오버 헤드를 최소화하기 위해 이전에 수행 한 작업을 추적 할 수있는 많은 스마트 기능이 있습니다. 더 많은 통찰력을위한 구글 "동적 언어 런타임". 그리고 아니요, '기본'루프의 속도에 결코 접근하지 않습니다.
Hans Passant 2011 년

답변:


234

동적으로 컴파일러가 다시 실행되도록 읽었지만 그 기능은 무엇입니까? 매개 변수로 사용 된 동적 또는 동적 동작 / 컨텍스트 (?)가있는 행을 사용하여 전체 메소드를 다시 컴파일해야합니까?

여기에 거래가 있습니다.

프로그램에서 동적 유형 인 모든 표현식 에 대해 컴파일러는 작업을 나타내는 단일 "동적 호출 사이트 객체"를 생성하는 코드를 내 보냅니다. 예를 들어 다음과 같은 경우 :

class C
{
    void M()
    {
        dynamic d1 = whatever;
        dynamic d2 = d1.Foo();

그러면 컴파일러는 도덕적으로 이와 같은 코드를 생성합니다. (실제 코드는 좀 더 복잡합니다. 이것은 프리젠 테이션을 위해 단순화되었습니다.)

class C
{
    static DynamicCallSite FooCallSite;
    void M()
    {
        object d1 = whatever;
        object d2;
        if (FooCallSite == null) FooCallSite = new DynamicCallSite();
        d2 = FooCallSite.DoInvocation("Foo", d1);

이것이 지금까지 어떻게 작동하는지 보십니까? M에 몇 번 전화를 걸어도 한 번만 호출 사이트를 생성합니다. 호출 사이트는 한 번 생성하면 영원히 유지됩니다. 호출 사이트는 "여기서 Foo에 대한 동적 호출이있을 것"을 나타내는 개체입니다.

이제 호출 사이트가 생겼으니 호출은 어떻게 작동합니까?

호출 사이트는 동적 언어 런타임의 일부입니다. DLR은 "음, 누군가가 여기이 객체에 대해 foo 메소드를 동적으로 호출하려고 시도하고 있습니다. 그것에 대해 아는 것이 있습니까? 아니오. 그러면 알아내는 것이 좋습니다."

그런 다음 DLR은 d1의 개체를 조사하여 그것이 특별한 지 확인합니다. 레거시 COM 객체, Iron Python 객체, Iron Ruby 객체 또는 IE DOM 객체 일 수 있습니다. 이들 중 하나가 아니면 일반 C # 개체 여야합니다.

이것은 컴파일러가 다시 시작되는 지점입니다. 렉서 나 파서가 필요하지 않으므로 DLR은 메타 데이터 분석기, 식에 대한 의미 분석기 및 IL 대신 식 트리를 내보내는 이미 터 만있는 C # 컴파일러의 특수 버전을 시작합니다.

메타 데이터 분석기는 Reflection을 사용하여 d1의 객체 유형을 결정한 다음 의미 론적 분석기에 전달하여 이러한 객체가 Foo 메서드에서 호출 될 때 어떤 일이 발생하는지 묻습니다. 오버로드 해결 분석기는이를 파악한 다음 식 트리 람다에서 Foo를 호출 한 것처럼 해당 호출을 나타내는 식 트리를 만듭니다.

그런 다음 C # 컴파일러는 캐시 정책과 함께 해당 식 트리를 DLR로 다시 전달합니다. 정책은 일반적으로 "이 유형의 개체를 두 번째로 볼 때 다시 전화를 걸지 않고이 식 트리를 다시 사용할 수 있습니다"입니다. 그런 다음 DLR은 식 트리에서 Compile을 호출하여 식 트리 대 IL 컴파일러를 호출하고 델리게이트에서 동적으로 생성 된 IL 블록을 뱉어냅니다.

그런 다음 DLR은 호출 사이트 개체와 관련된 캐시에이 대리자를 캐시합니다.

그런 다음 대리자를 호출하고 Foo 호출이 발생합니다.

두 번째로 M에 전화하면 이미 콜 사이트가 있습니다. DLR은 개체를 다시 조사하고 개체가 지난번과 같은 형식이면 캐시에서 대리자를 가져와 호출합니다. 객체가 다른 유형이면 캐시가 누락되고 전체 프로세스가 다시 시작됩니다. 호출의 의미 분석을 수행하고 결과를 캐시에 저장합니다.

이것은 동적을 포함하는 모든 표현에서 발생합니다 . 예를 들어 다음과 같은 경우 :

int x = d1.Foo() + d2;

다음 거기에 세 가지 동적 호출 사이트. 하나는 Foo에 대한 동적 호출 용, 하나는 동적 추가 용, 하나는 dynamic에서 int 로의 동적 변환 용입니다. 각각에는 자체 런타임 분석과 분석 결과의 자체 캐시가 있습니다.

말이 되나?


호기심에서 파서 / 어휘 분석기가없는 특수 컴파일러 버전은 표준 csc.exe에 특수 플래그를 전달하여 호출됩니다.
Roman Royter 2011 년

@Eric, short, int 등의 암시 적 변환에 대해 이야기하는 이전 블로그 게시물을 알려주시겠습니까? 내가 거기에서 언급했듯이 Convert.ToXXX와 함께 dynamic을 사용하면 컴파일러가 실행되는 방법 / 이유가 있습니다. 나는 세부 사항을 도살하고 있다고 확신하지만, 내가 무슨 말을하는지 알기를 바랍니다.
Adam Rackis 2011 년

4
@Roman : 아니요. csc.exe는 C ++로 작성되었으며 C #에서 쉽게 호출 할 수있는 것이 필요했습니다. 또한 메인 라인 컴파일러에는 자체 유형 객체가 있지만 Reflection 유형 객체를 사용할 수 있어야했습니다. csc.exe 컴파일러에서 C ++ 코드의 관련 부분을 추출하여 한 줄씩 C #으로 변환 한 다음 DLR이 호출 할 라이브러리를 빌드했습니다.
Eric Lippert 2011 년

9
@Eric, "우리는 csc.exe 컴파일러에서 C ++ 코드의 관련 부분을 추출하여 한 줄씩 C #으로 번역했습니다."그 당시 사람들은 Roslyn이 추구 할 가치가 있다고 생각했습니다. :)
ShuggyCoUk

5
@ShuggyCoUk : 서비스로서의 컴파일러를 사용한다는 아이디어가 한동안 떠오르고 있었지만 실제로는 코드 분석을 위해 런타임 서비스가 필요하다는 것이 그 프로젝트에 큰 자극제가되었습니다.
Eric Lippert 2011 년

108

업데이트 : 사전 컴파일 및 지연 컴파일 된 벤치 마크 추가

업데이트 2 : 밝혀졌다, 내가 틀렸다. 완전하고 정확한 답변은 Eric Lippert의 게시물을 참조하십시오. 벤치 마크 수치를 위해 여기에 남겨 두겠습니다.

* 업데이트 3 : Mark Gravell의이 질문에 대한 답변을 기반으로 IL-Emitted 및 Lazy IL-Emitted 벤치 마크가 추가되었습니다 .

내가 아는 한 dynamic키워드를 사용 한다고해서 런타임에 추가 컴파일이 발생하지는 않습니다 (동적 변수를 지원하는 개체 유형에 따라 특정 상황에서 그렇게 할 수 있다고 생각합니다).

성능과 관련하여 dynamic본질적으로 약간의 오버 헤드가 발생하지만 생각만큼 많지는 않습니다. 예를 들어 다음과 같은 벤치 마크를 실행했습니다.

void Main()
{
    Foo foo = new Foo();
    var args = new object[0];
    var method = typeof(Foo).GetMethod("DoSomething");
    dynamic dfoo = foo;
    var precompiled = 
        Expression.Lambda<Action>(
            Expression.Call(Expression.Constant(foo), method))
        .Compile();
    var lazyCompiled = new Lazy<Action>(() =>
        Expression.Lambda<Action>(
            Expression.Call(Expression.Constant(foo), method))
        .Compile(), false);
    var wrapped = Wrap(method);
    var lazyWrapped = new Lazy<Func<object, object[], object>>(() => Wrap(method), false);
    var actions = new[]
    {
        new TimedAction("Direct", () => 
        {
            foo.DoSomething();
        }),
        new TimedAction("Dynamic", () => 
        {
            dfoo.DoSomething();
        }),
        new TimedAction("Reflection", () => 
        {
            method.Invoke(foo, args);
        }),
        new TimedAction("Precompiled", () => 
        {
            precompiled();
        }),
        new TimedAction("LazyCompiled", () => 
        {
            lazyCompiled.Value();
        }),
        new TimedAction("ILEmitted", () => 
        {
            wrapped(foo, null);
        }),
        new TimedAction("LazyILEmitted", () => 
        {
            lazyWrapped.Value(foo, null);
        }),
    };
    TimeActions(1000000, actions);
}

class Foo{
    public void DoSomething(){}
}

static Func<object, object[], object> Wrap(MethodInfo method)
{
    var dm = new DynamicMethod(method.Name, typeof(object), new Type[] {
        typeof(object), typeof(object[])
    }, method.DeclaringType, true);
    var il = dm.GetILGenerator();

    if (!method.IsStatic)
    {
        il.Emit(OpCodes.Ldarg_0);
        il.Emit(OpCodes.Unbox_Any, method.DeclaringType);
    }
    var parameters = method.GetParameters();
    for (int i = 0; i < parameters.Length; i++)
    {
        il.Emit(OpCodes.Ldarg_1);
        il.Emit(OpCodes.Ldc_I4, i);
        il.Emit(OpCodes.Ldelem_Ref);
        il.Emit(OpCodes.Unbox_Any, parameters[i].ParameterType);
    }
    il.EmitCall(method.IsStatic || method.DeclaringType.IsValueType ?
        OpCodes.Call : OpCodes.Callvirt, method, null);
    if (method.ReturnType == null || method.ReturnType == typeof(void))
    {
        il.Emit(OpCodes.Ldnull);
    }
    else if (method.ReturnType.IsValueType)
    {
        il.Emit(OpCodes.Box, method.ReturnType);
    }
    il.Emit(OpCodes.Ret);
    return (Func<object, object[], object>)dm.CreateDelegate(typeof(Func<object, object[], object>));
}

코드에서 알 수 있듯이 간단한 no-op 메서드를 7 가지 방법으로 호출하려고합니다.

  1. 직접 메서드 호출
  2. 사용 dynamic
  3. 반사로
  4. Action런타임에 미리 컴파일 된를 사용 합니다 (따라서 결과에서 컴파일 시간 제외).
  5. 사용 Action(따라서, 컴파일 시간 포함)이 아닌 스레드 안전 레이지 변수를 사용하여, 필요 처음 컴파일을 가도록
  6. 테스트 전에 생성되는 동적 생성 방법을 사용합니다.
  7. 테스트 중에 느리게 인스턴스화되는 동적 생성 메서드를 사용합니다.

각각은 간단한 루프에서 백만 번 호출됩니다. 타이밍 결과는 다음과 같습니다.

직접 : 3.4248ms
동적 : 45.0728ms
반사 : 888.4011ms
사전 컴파일 : 21.9166ms
LazyCompiled : 30.2045ms
ILEmitted : 8.4918ms
LazyILEmitted : 14.3483ms

따라서 dynamic키워드 를 사용하는 것은 메서드를 직접 호출하는 것보다 훨씬 더 오래 걸리지 만, 여전히 약 50 밀리 초 내에 작업을 백만 번 완료 할 수 있으므로 리플렉션보다 훨씬 빠릅니다. 우리가 호출하는 메서드가 몇 개의 문자열을 함께 결합하거나 값을 찾기 위해 컬렉션을 검색하는 것과 같이 집약적 인 작업을 수행하려는 경우 이러한 작업이 직접 호출과 호출 간의 차이보다 훨씬 더 큽니다 dynamic.

성능은 dynamic불필요하게 사용 하지 않는 여러 가지 좋은 이유 중 하나 일 뿐이지 만 진정한 dynamic데이터를 처리 할 때 단점보다 훨씬 더 큰 장점을 제공 할 수 있습니다.

업데이트 4

Johnbot의 의견에 따라 Reflection 영역을 4 개의 개별 테스트로 나누었습니다.

    new TimedAction("Reflection, find method", () => 
    {
        typeof(Foo).GetMethod("DoSomething").Invoke(foo, args);
    }),
    new TimedAction("Reflection, predetermined method", () => 
    {
        method.Invoke(foo, args);
    }),
    new TimedAction("Reflection, create a delegate", () => 
    {
        ((Action)method.CreateDelegate(typeof(Action), foo)).Invoke();
    }),
    new TimedAction("Reflection, cached delegate", () => 
    {
        methodDelegate.Invoke();
    }),

... 다음은 벤치 마크 결과입니다.

여기에 이미지 설명 입력

따라서 많이 호출해야하는 특정 메서드를 미리 결정할 수있는 경우 해당 메서드를 참조하는 캐시 된 대리자를 호출하는 것은 메서드 자체를 호출하는 것만 큼 빠릅니다. 그러나 호출 할 때 호출 할 메서드를 결정해야하는 경우 대리자를 만드는 데 비용이 많이 듭니다.


2
자세한 답변 감사합니다! 실제 수치도 궁금했습니다.
Sergey Sirotkin 2011 년

4
음, 동적 코드는 컴파일러의 메타 데이터 임포터, 시맨틱 분석기 및 표현식 트리 이미 터를 시작한 다음 그 출력에 대해 expression-tree-to-il 컴파일러를 실행하므로 시작한다고 말하는 것이 공정하다고 생각합니다. 런타임에 컴파일러를 작동시킵니다. 어휘 분석기를 실행하지 않기 때문에 파서는 거의 관련이 없어 보입니다.
Eric Lippert 2011 년

6
성능 수치는 DLR의 공격적인 캐싱 정책이 어떻게 효과가 있는지 확실히 보여줍니다. 예를 들어 호출을 할 때마다 다른 수신 유형이있는 경우와 같이 예제가 어리석은 작업을 수행 한 경우 이전에 컴파일 된 분석 결과의 캐시를 활용할 수 없을 때 동적 버전이 매우 느리다는 것을 알 수 있습니다. . 그러나 그것을 이용할 수 있을 때 , 거룩한 선하심은 그것이 항상 빠르다는 것입니다.
Eric Lippert 2011 년

1
Eric의 제안에 따라 뭔가 구피. 주석 처리 된 줄을 바꾸어 테스트합니다. 8964ms vs 814ms, dynamic물론 패배 :public class ONE<T>{public object i { get; set; }public ONE(){i = typeof(T).ToString();}public object make(int ix){ if (ix == 0) return i;ONE<ONE<T>> x = new ONE<ONE<T>>();/*dynamic x = new ONE<ONE<T>>();*/return x.make(ix - 1);}}ONE<END> x = new ONE<END>();string lucky;Stopwatch sw = new Stopwatch();sw.Start();lucky = (string)x.make(500);sw.Stop();Trace.WriteLine(sw.ElapsedMilliseconds);Trace.WriteLine(lucky);
Brian

1
공정하게 반영하고 메서드 정보에서 대리자를 만듭니다.var methodDelegate = (Action)method.CreateDelegate(typeof(Action), foo);
Johnbot
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.