W poprzednim poście napisałem kilka słów o dwóch sposobach wywoływania konstruktorów statycznych. Dziś chciałbym pokazać, że faktycznie ma to wpływ na wydajność. Rozważmy następujący przykład:
public class BeforeInitSementics { public static int Value = 10; } public class PreciseSemantics { public static int Value; static PreciseSemantics() { Value = 20; } } internal class Program { private const int Iterations = 100000000; private static void Test1() { // Precise Stopwatch stopwatch = Stopwatch.StartNew(); for (int i = 0; i < Iterations; i++) { PreciseSemantics.Value = 10; } stopwatch.Stop(); Console.WriteLine("Precise:{0}",stopwatch.ElapsedMilliseconds); // Before-init stopwatch = Stopwatch.StartNew(); for (int i = 0; i < Iterations; i++) { BeforeInitSementics.Value = 10; } stopwatch.Stop(); Console.WriteLine("Before-init:{0}", stopwatch.ElapsedMilliseconds); } private static void Main(string[] args) { Test1(); } }
W trybie Release na moim komputerze uzyskałem wynik 567 oraz 152. Różnica jest więc dość znacząca (kilkukrotna). Jak wiemy z poprzedniego wpisu, semantyka BeforeInit pozwala wyemitować wywołanie konstruktora w jakimkolwiek momencie. Kompilator jest więc na tyle sprytny, że emituje to przed pętlą for (tak więc cała logika będzie wykonana tylko raz). W przypadku semantyki precise musi to nastąpić linię przed dostępem do pierwszego pola. Z tego wynika, że podejście precise będzie dużo wolniejsze – w każdej iteracji musi zostać wykonana pewna logika taka jak synchronizacja, sprawdzenie czy konstruktor został już wywołany itp.
Rozważmy kolejny przykład:
public class BeforeInitSementics { public static int Value = 10; } public class PreciseSemantics { public static int Value; static PreciseSemantics() { Value = 20; } } internal class Program { private const int Iterations = 100000000; private static void Test1() { // Precise Stopwatch stopwatch = Stopwatch.StartNew(); for (int i = 0; i < Iterations; i++) { PreciseSemantics.Value = 10; } stopwatch.Stop(); Console.WriteLine("Precise:{0}",stopwatch.ElapsedMilliseconds); // Before-init stopwatch = Stopwatch.StartNew(); for (int i = 0; i < Iterations; i++) { BeforeInitSementics.Value = 10; } stopwatch.Stop(); Console.WriteLine("Before-init:{0}", stopwatch.ElapsedMilliseconds); } private static void Test2() { // Precise Stopwatch stopwatch = Stopwatch.StartNew(); for (int i = 0; i < Iterations; i++) { PreciseSemantics.Value = 10; } stopwatch.Stop(); Console.WriteLine("Precise:{0}", stopwatch.ElapsedMilliseconds); // Before-init stopwatch = Stopwatch.StartNew(); for (int i = 0; i < Iterations; i++) { BeforeInitSementics.Value = 10; } stopwatch.Stop(); Console.WriteLine("Before-init:{0}", stopwatch.ElapsedMilliseconds); } private static void Main(string[] args) { Test1(); Test2(); } }
Test 1 wydrukuje takie same liczby jak w kroku I (567,151). Test2 z kolei wyświetli dwie podobne wartości (np. 151,151). Dlaczego? Wynika to z zasady emitowania wywołań do konstruktora. Kompilując metodę, kompilator sprawdza czy konstruktor został już wywołany. Jeśli tak, nie trzeba już emitować żadnego kodu JIT wywołującego konstruktor statyczny. W Test1 kod taki został wyemitowany ponieważ na tamtym etapie nie było jeszcze żadnego wywołania. Wyemitowany kod to m.in. synchronizacja, sprawdzenie czy konstruktor został już wywołany itp. W Test2 jest pewność, że konstruktor statyczny został wykonany (w Test1), zatem nie trzeba emitować jakiejkolwiek logiki. W takim przypadku Before i Precise zachowują się dokładnie tak samo.