dynamic
C # 의 성능에 대한 질문이 있습니다 . 읽은 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));
}
답변
동적으로 컴파일러가 다시 실행되도록 읽었지만 그 기능은 무엇입니까? 매개 변수로 사용 된 동적 또는 동적 동작 / 컨텍스트 (?)가있는 행을 사용하여 전체 메소드를 다시 컴파일해야합니까?
여기에 거래가 있습니다.
프로그램에서 동적 유형 인 모든 표현식 에 대해 컴파일러는 작업을 나타내는 단일 “동적 호출 사이트 객체”를 생성하는 코드를 내 보냅니다. 예를 들어 다음과 같은 경우 :
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 로의 동적 변환 용입니다. 각각에는 자체 런타임 분석과 분석 결과의 자체 캐시가 있습니다.
말이 되나?
답변
업데이트 : 사전 컴파일 및 지연 컴파일 된 벤치 마크 추가
업데이트 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 가지 방법으로 호출하려고합니다.
- 직접 메서드 호출
- 사용
dynamic
- 반사로
Action
런타임에 미리 컴파일 된를 사용 합니다 (따라서 결과에서 컴파일 시간 제외).- 사용
Action
(따라서, 컴파일 시간 포함)이 아닌 스레드 안전 레이지 변수를 사용하여, 필요 처음 컴파일을 가도록 - 테스트 전에 생성되는 동적 생성 방법을 사용합니다.
- 테스트 중에 느리게 인스턴스화되는 동적 생성 메서드를 사용합니다.
각각은 간단한 루프에서 백만 번 호출됩니다. 타이밍 결과는 다음과 같습니다.
직접 : 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();
}),
… 다음은 벤치 마크 결과입니다.
따라서 많이 호출해야하는 특정 메서드를 미리 결정할 수있는 경우 해당 메서드를 참조하는 캐시 된 대리자를 호출하는 것은 메서드 자체를 호출하는 것만 큼 빠릅니다. 그러나 호출 할 때 호출 할 메서드를 결정해야하는 경우 대리자를 만드는 데 비용이 많이 듭니다.