Statyczne konstruktory–wydajność część II

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.

Leave a Reply

Your email address will not be published.