Dynamic 타입
Dynamic
리플렉션이나 다른 구성요소와 통신하는 일을 더 쉽게할 수 있도록 C# 컴파일러는 표현식의 타입을 dynamic으로 선언할 수 있게 했다.
만약 코드에서 dynamic으로 표시된 표현식이나 멤버를 호출하는 코드를 작성하면, 컴파일러는 페이로드(Payload)라 불리는 특별한 IL 코드를 생성한다.
실행 시점에서 페이로드 코드는 객체의 실제 타입을 기반으로 정확한 연산을 찾아 실행하는 일을 한다.
public class Cat
{
public void Speak()
{
Console.WriteLine("Meow");
}
}
public class Dog
{
public void Speak()
{
Console.WriteLine("Woof");
}
}
public class Rock
{
}
//...
dynamic target;
target = new Cat();
target.Speak(); //Console : "Meow"
target = new Dog();
target.Speak(); //Console : "Woof"
target = new Rock();
target.Speak(); //RuntimeBinderException
컴파일러는 dynamic 타입을 사실상 System.Object로 취급한다. 거기에 추가로 몇몇 속성(DynamicAttribute)등을 추가하여 메타데이터상에 흔적을 남긴다.
dynamic 타입은 사실상 System.Object이므로 당연하게도 값 타입은 박싱/언박싱이 일어난다.
만약 dynamic 표현식이 foreach문장이나 using 문장에서 사용하는 리소스에 지정되었다면, 컴파일러는 비제네릭 타입의 IEnumerable이나 IDisposable인터페이스에 대응되도록 코드를 생성한다.
페이로드 코드는 런타임 바인더라고 알려진 클래스를 사용한다. 그리고 페이로드 코드가 실행될 때 dynamic코드가 실행 시점에 만들어지게 되어 메모리 내에 어셈블리가 추가된다.
주의사항
Dynamic은 내부적으로 Reflection을 사용하고 있다. (출처 : https://www.atmosera.com/blog/cant-use-dynamic-c-keyword-xamarin-ios/) 특히 Dynamic이 사용하는 Reflection.Emit은 런타임 중에 코드를 생성하기 때문에 일부 플랫폼이나 환경에서는 사용이 불가능하다. (예. iOS 정책, AOT컴파일러(IL2CPP))
Dynamic vs Reflection
Reflection | Dynamic | |
Inspect (meta-data) | Yes | No |
Invoke public members | Yes | Yes |
Invoke private members | Yes | No |
Caching | No | Yes |
Static class | Yes | No |
Reflection은 메타데이터를 볼수도 있고(inspect) 호출(invoke)할 수도 있지만, dynamic은 오직 호출만 가능하다. 거기에 Dynamic은 private멤버나 static클래스에 접근할 수 없다.
다른 점은 dynamic은 내부적으로 메서드 콜을 캐싱한다.
성능 비교
테스트 코드
object aaa = new AAA(); //"Foo"라는 이름의 메서드 존재
object bbb = new BBB(); //"Foo"라는 이름의 메서드 존재
object[] objects = new[] { aaa, bbb };
Stopwatch sw = new Stopwatch();
int iter = 400_000;
sw.Restart();
for (int i = 0; i < iter; ++i)
{
for (int oi = 0; oi < objects.Length; ++oi)
{
var obj = objects[oi];
if (obj is AAA objA)
objA.Foo();
else if (obj is BBB objB)
objB.Foo();
}
}
sw.Stop();
Console.WriteLine($"casting takes ... {sw.ElapsedMilliseconds}ms");
sw.Restart();
for (int i = 0; i < iter; ++i)
{
for (int oi = 0; oi < objects.Length; ++oi)
{
var obj = objects[oi];
var method = obj.GetType().GetMethod("Foo");
method.Invoke(obj, null);
}
}
sw.Stop();
Console.WriteLine($"Reflection takes ... {sw.ElapsedMilliseconds}ms");
Type[] cachedTypes = new[] { aaa.GetType(), bbb.GetType() };
sw.Restart();
for (int i = 0; i < iter; ++i)
{
for (int oi = 0; oi < objects.Length; ++oi)
{
var obj = objects[oi];
var type = cachedTypes[oi];
var method = type.GetMethod("Foo");
method.Invoke(obj, null);
}
}
sw.Stop();
Console.WriteLine($"Reflection (type caching) takes ... {sw.ElapsedMilliseconds}ms");
MethodInfo[] methodInfos = new[] { aaa.GetType().GetMethod("Foo"), bbb.GetType().GetMethod("Foo") };
sw.Restart();
for (int i = 0; i < iter; ++i)
{
for (int oi = 0; oi < objects.Length; ++oi)
{
var obj = objects[oi];
var method = methodInfos[oi];
method.Invoke(obj, null);
}
}
sw.Stop();
Console.WriteLine($"Reflection (method info caching) takes ... {sw.ElapsedMilliseconds}ms");
sw.Restart();
for (int i = 0; i < iter; ++i)
{
for (int oi = 0; oi < objects.Length; ++oi)
{
var obj = objects[oi];
dynamic target = obj;
target.Foo();
}
}
sw.Stop();
Console.WriteLine($"dynamic takes ... {sw.ElapsedMilliseconds}ms");
반복횟수 \ 시간 | Static Cast | Reflection | 타입 캐싱 | MethodInfo 캐싱 | Dynamic |
1 | 0ms | 0ms | 0ms | 0ms | 43ms |
100,000 | 1ms | 32ms | 32ms | 18ms | 50ms |
400,000 | 5ms | 130ms | 128ms | 71ms | 72ms |
10,000,000 | 133ms | 3270ms | 3265ms | 1803ms | 703ms |
Dynamic의 경우 최초 호출시 엄청나게 시간을 잡아먹는다. 약 40만번의 호출부터 MethodInfo를 캐싱한 Reflection과 수행 시간이 비슷해진다. (단, 최초 로드 시간을 제외한다면 항상 훨씬 빠르다.)
참고
CLR via C# 4판
https://www.codeproject.com/Articles/593881/What-is-the-Difference-between-Reflection-and-Dyna