Statyczne konstruktory–wydajność

Konstruktory statyczne zwykłe służą do inicjalizowania pól statycznych lub walidacji typów generycznych np.:

class Generic<T> where T: struct
{
    static Generic() {
        if (!typeof(T).IsEnum) {
            throw new ArgumentException("T must be an enum");
        }
    }
}

Dobrą informacją jest fakt, że statyczne konsturktory są thread-safe co ułatwia implementację pewnych wzorów projektowych. Domyślnie statyczne konstuktory są generowane wyłącznie gdy dana klasa posiada jakieś pola statyczne. Pytanie brzmi: kiedy konstruktor statyczny jest wywoływany?

CLR gwarantuje tylko, że:

  1. operacje w konstruktorze są thread-safe,
  2. konstruktor zostanie wywołany przed dostępem do pierwszego pola danej klasy. CLR nie określa kiedy dokładnie do wywołania dojdzie.

Punkt pierwszy powinien być jasny ale drugi wymaga z pewnością rozwinięcia. Istnieją dwa możliwe przypadki:

  1. Konstruktor może zostać wywołany od razu przed dostępem do danego pola (precise semantics).
  2. Konstruktor może być wywołany w jakimkolwiek momencie  przed dostępem do pola (before-init-semantics). Zgodnie z tym podejściem, konstruktor może zostać wywołany DUŻO wcześniej niż tego byśmy spodziewali się.

Preferowane jest drugie podejście ponieważ daje więcej swobody CLR i dzięki temu wywołanie może zostać zoptymalizowane. Programiści nie mogą jawnie określić sposobu wywołania konstruktora ale istnieją pewne zasady, które ułatwią zrozumienie jak CLR wybiera odpowiednią semantykę. Przykład:

public class BeforeInitSemantics
{
   public static int Value = 10;
}
public class PreciseSemantics
{
   public static int Value;
   static PreciseSemantics()
   {
       Value = 20;
   }    
}

Pierwsza klasa zostanie oznaczona jako before-init ponieważ konstruktor nie jest jawnie zdefiniowany. Przez to zostanie wygenerowany automatycznie (to właśnie w nim Value będzie ustawiona na 10). Taka sytuacja jest najoptymalniejsza ponieważ CLR sam zadba o jak najwyższą wydajność. Można o tym przekonać się zaglądając do IL:

.class auto ansi nested public beforefieldinit BeforeInitSementics
    extends [mscorlib]System.Object
{
    .method private hidebysig specialname rtspecialname static void .cctor() cil managed
    {
    }

    .method public hidebysig specialname rtspecialname instance void .ctor() cil managed
    {
    }
    .field public static int32 Value
}

.method public hidebysig specialname rtspecialname instance void .ctor() cil managed
{
    .maxstack 8
    L_0000: ldarg.0 
    L_0001: call instance void [mscorlib]System.Object::.ctor()
    L_0006: ret 
}

Pierwsza klasa została oznaczona flagą beforefieldinit. Reguła jest prosta i wszystkie klasy z jawnymi konstruktorami nie będą opatrzone flagą beforefieldinit. Gdyby umożliwić wywołanie jawnych konstruktorów, które zawierają nieznana logikę, mogłoby to spowodować efekty uboczne. W pełni jest to zrozumiałe, ale  czy nie byłoby lepiej gdyby użytkownicy samu również mogli decydować o tym? Programista, który zaimplementował dany konstruktor zdaje sobie sprawę czy logika w nim zawarta może przynieść efekty uboczne.

W następnym poście pokażę, że różnice w wydajności mogą być dość znaczne i dla pewnych systemów ma to znaczenie.

4 thoughts on “Statyczne konstruktory–wydajność”

  1. Bardzo czytelnie i fajnie opisane. Fajnie by było jak byś do tego tematu dodał przykład z Singletonem statycznym ponieważ zauważam że bardzo wiele ludzi implementuje go błędnie własnie z powodu niuansu, który opisujesz:

    Przykład:

    //we will be marked and we will be consived in a race.
    public static Singleton
    {
    private Singleton() {}

    private static readonly instance = new Singleton();

    public Singleton { get { return instance; } }
    }

    Nie jest poprawny 🙂

  2. @badamczewski:
    Dlaczego niepoprawny? Instancja może zostać stworzona kiedykolwiek ale czy to ma zawsze znaczenie? Masz na myśli przypadek kiedy konstruktor singleton’a faktycznie musi zostać wykonany w danym momencie?

  3. Z teoretycznego punktu widzenia kod jest poprawny posiada jednak pewne podstawowe wady:

    Instancja takiego singletona nie jest do końca lazy (czasami jest eager), w przypadku gdy konstruktor zostanie wywołany w dowolnym miejscu w czasie, jeśli zależy od momentu wykonania (jak wspomniałeś) spowoduje to nie mały problem.

    w .NET 4.0 z tego co mi wiadomo wyeliminowano zachowanie eager z atrybutu.

    Oto przykładowy kod gdzie wszystko może się stać 🙂

    public class Singleton
    {
    private Singleton() { Console.WriteLine(“Ctor”); }

    private static readonly Singleton i = new Singleton();

    public static Singleton Instance { get { Console.WriteLine(“X”); return i; } }

    }

    class Program
    {
    static void Main(string[] args)
    {
    Console.WriteLine(“Start”);
    Singleton s = Singleton.Instance;

    Console.ReadKey();
    }
    }

    Możemy dostać albo:

    Start
    Ctor
    X

    albo:

    Ctor
    Start
    X

    albo też:

    Start
    X
    Ctor

Leave a Reply

Your email address will not be published.