W zeszłym tygodniu pisałem o zastosowaniu dynamicznych zmiennych. W dzisiejszym wpisie zastanowimy się co dokładnie CLR robi z dynamic i jak to wpływa na wydajność aplikacji. Pierwszy test polega na porównaniu wydajności dodawania dwóch liczb:
private static void TestStatic() { var stopwatch = Stopwatch.StartNew(); int a = 10; int b = 45; int c = a + b; stopwatch.Stop(); Console.WriteLine("Static:{0}", stopwatch.ElapsedTicks); } private static void TestDynamic() { var stopwatch = Stopwatch.StartNew(); dynamic a = 10; dynamic b = 45; dynamic c = a + b; stopwatch.Stop(); Console.WriteLine("Dynamic:{0}",stopwatch.ElapsedTicks); }
TestStatic bada wydajność w przypadku klasycznego wykonania kodu – wszystkie typy zmiennych znane już są na etapie kompilacji. Z kolei TestDynamic używa słowa kluczowego dynamic co oznacza, że typ pola zostanie wyznaczony dopiero w trakcie wykonywania kodu. Wykonując pierwszy raz powyższe metody otrzymano następujące wyniki:
Static: 2
Dynamic: 125810
Różnica jest oczywiście kolosalna i nieakceptowalna w większości przypadków. Dynamiczne rozwiązanie okazało się wolniejsze o ponad 60 000 razy! Aby w pełni zrozumieć otrzymany wynik należy przeanalizować jak działa DLR. Gdy pierwszy raz wykonywana jest metoda TestDynamic, DLR sprawdza czy została już ta metoda wcześniej skompilowana. Oczywiście za pierwszym razem nie została więc używając specjalnego kompilatora sprawdzany jest typ i wstawiany jest silnie typowany (w tym przypadku int). Wynik jest buforowany tak, że następne wywołania nie muszą już wykonywać tej logiki. Dla CLR “skompilowany” kod niczym nie różni się od TestStatic. Słowo dynamic przeznaczone jest dla DLR a nie CLR – w momencie wykonywania kodu dynamic jest zastępowany normalnym, statycznym typem.
Z powyższych rozważań wynika, że następne wywołania TestDynamic powinny być wydajnościowo zbliżone do TestStatic. Sprawdźmy to:
private static void Main(string[] args) { TestStatic(); // szybkie TestDynamic();//wolne TestStatic(); // szybkie TestDynamic();// szybkie również! }
Wynik to 2 dla TestStatic oraz 3 dla TestDynamic. Oczywiście to zbieg okoliczności, że TestDynamic ma wartość trochę większą. Należy traktować obydwa wywołania tak jakby miały identyczny lub BARDZO zbliżony poziom wydajności. Wniosek jest taki, że dla metod, które często są wywoływane dynamic może być dobrym rozwiązaniem. W przypadku pojedynczego zastosowania zbyt wiele czasu jest poświęcane przez DLR i korzyści z zastosowania dynamic nie są wystarczające aby z niego skorzystać.
Oczywiście powyższe przykłady nie są praktycznie i dla prostych operacji typu dodanie dwóch liczb zawsze lepiej wykorzystać statyczne typowanie. W poprzednich postach pokazałem kilka praktycznych zastosowań. Istnieje jednak jeszcze bardzo ważny scenariusz użycia dla dynamic – mechanizm refleksji. Korzystanie z niego jest dość trudne i powstały kod jest bardzo trudny w czytaniu. Sytuacja jest szczególnie zła w przypadku wykonywania właściwości typu indexer. Należy zaznaczyć, że refleksja jest wolniejsza niż klasyczne wywołanie metod. Z tego względu poniższe metody testują wydajność 3 scenariuszy:
- Klasyczne wywołanie metody.
- Wywołanie metody za pomocą refleksji.
- Wywołanie metody za pomocą słowa kluczowego dynamic.
Test:
internal class Program { private const int N = 10000000; private static void TestReflectionInvoke() { MethodInfo toUpper = typeof(string).GetMethod("ToUpper",new Type[0]); var stopwatch = Stopwatch.StartNew(); object test = "Hello World"; for (int i = 0; i < N; i++) test = toUpper.Invoke(test, null); stopwatch.Stop(); Console.WriteLine("Reflection:{0}", stopwatch.ElapsedTicks); } private static void TestStaticInvoke() { var stopwatch = Stopwatch.StartNew(); string test = "Hello World"; for (int i = 0; i < N; i++) test = test.ToUpper(); stopwatch.Stop(); Console.WriteLine("Static:{0}", stopwatch.ElapsedTicks); } private static void TestDynamicInvoke() { var stopwatch = Stopwatch.StartNew(); dynamic test = "Hello World"; for (int i = 0; i < N;i++ ) test = test.ToUpper(); stopwatch.Stop(); Console.WriteLine("Dynamic:{0}", stopwatch.ElapsedTicks); } private static void Main(string[] args) { TestStaticInvoke(); TestDynamicInvoke(); TestReflectionInvoke(); } }
Wyniki:
Static:15549636
Dynamic:15835424
Reflection:20069418
Dynamic wygrywa z refleksją ponieważ metoda została wykonana wiele razy. W przypadku gdy N=1 refleksja jest znacząco szybsza. Dynamic jednak znacząco wygrywa z reflection gdy DLR zbuforował już kod (następne wykonania). Pierwsze wykonanie jest zawsze wolniejsze od wywołania statycznego i refleksji. Ponadto łatwiej jest korzystać dynamic pisząc po prostu:
test = test.ToUpper();
Zamiast:
MethodInfo toUpper = typeof(string).GetMethod("ToUpper",new Type[0]); test = toUpper.Invoke(test, null);
Ponadto warto zaobserwować w oknie output, że przy pierwszym wykonaniu dynamic ładowane są dodatkowe biblioteki:
'ConsoleApplication5.vshost.exe' (Managed (v4.0.30319)): Loaded 'C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System.Dynamic\v4.0_4.0.0.0__b03f5f7f11d50a3a\System.Dynamic.dll' 'ConsoleApplication5.vshost.exe' (Managed (v4.0.30319)): Loaded 'Anonymously Hosted DynamicMethods Assembly'