W .NET można oznaczyć klasy słowem kluczowym sealed co powoduje, że po klasie nie można dziedziczyć. Poza tą właściwością, sealed ma jeszcze jedną ważna zaletę - poprawia wydajność wywołań metod wirtualnych 😲.

sealed vs non-sealed - Wydajność

Spójrzmy na prosty kodzik:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class BaseClass
{
    public virtual int Method() => 5;
}

public class NonSealedClass : BaseClass
{
    public override int Method() => 22;
}

public sealed class SealedClass : BaseClass
{
    public override int Method() => 11;
}

Dwie klasy dziedziczą po BaseClass i jedyna różnica pomiędzy nimi to słówko kluczowe sealed. Sprawdźmy teraz różnice w wydajności przy pomocy BenchmarkDotNet i następującego kodu:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class SealedBenchmark
{
    SealedClass sealedClass = new();

    NonSealedClass nonSealedClass = new();

    [Benchmark]
    public int SealedClass()
    {
        return sealedClass.Method() + 1337;
    }

    [Benchmark]
    public int NonSealedClass()
    {
        return nonSealedClass.Method() + 1337;
    }
}

Wynik:

Wywołanie metody wirtualnej z SealedClass jest w tym przypadku około 162 razy szybsze ⚡ niż dla klasy która nie jest sealed.

Dlaczego tak się dzieje

Za tym wszystkim stoi tzw. Virtual method table - upraszczając: podczas tworzenia instancji klasy, która ma metody wirtualne, kompilator dodaje tablice z metodami wirtualnymi. Tablice te są używana podczas runtime, aby określić która metoda ma być faktycznie użyta (z klasy bazowej czy z klas pochodnych), bo w czasie kompilacji kompilator może jeszcze tego nie widzieć. Natomiast jeżeli klasa jest zapieczętowana (sealed) to runtime dokładnie wie którą metodę użyć i nie korzysta już z Virtual method table.

Oczywiście są sytuacje w których kompilator wie, że dana instancja klasy jest konkretnego typu i wtedy już na poziomie kompilacji potrafi określić która metoda powinna być użyta, wtedy nie ma różnicy w wydajności pomiędzy sealed a nonSealed, przykład:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class SealedBenchmark
{
    [Benchmark]
    public int SealedClassDirectUse()
    {
        SealedClass sealedClass = new();
        return sealedClass.Method() + 1337;
    }

    [Benchmark]
    public int NonSealedClassDirectUse()
    {
        NonSealedClass nonSealedClass = new();
        // Kompilator widzi linijke wyżej inicjalizacje klasa NonSealedClass i dlatego wie, że
        // ma wywołać metode wirtualną dokładnie z tej klasy.
        return nonSealedClass.Method() + 1337;
    }
}

Wynik:

Rzutowanie też jest szybsze

Tym razem bez rozpisywania się. Kodzik i wynik:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class SealedCastBenchmark
{
    BaseClass baseClass = new();

    [Benchmark]
    public bool SealedClass()
    {
        return baseClass is SealedClass;
    }

    [Benchmark]
    public bool NonSealedClass()
    {
        return baseClass is NonSealedClass;
    }
}

Podsumowanie

Oznaczenie klas słówkiem sealed jest bardzo tanim sposobem na optymalizacje naszego kodu. Oczywiście, różnice są w nanosekundach i można powiedzieć, że ostatecznie to nie zrobi większej różnicy na ogólnej wydajności naszej aplikacji, ale chyba lepiej żeby działała trochę szybciej niż trochę wolniej? 😉Tym bardziej, że w samym kodzie dotneta zachodzą takie zmiany: https://github.com/dotnet/runtime/pull/50225 - setki klas zostało oznaczone jako sealed.

Pomocne linki