눈팅하는 게임개발자 블로그

[C#] Performance benefits of sealed class in .NET 본문

공부한거/Unity

[C#] Performance benefits of sealed class in .NET

Palamore 2022. 3. 23. 22:08

원문

https://www.meziantou.net/performance-benefits-of-sealed-class.htm

 

Performance benefits of sealed class in .NET - Gérald Barré

In this post, I describe the performance benefits of sealed classes in .NET.

www.meziantou.net

 

.Net 환경에서 Sealed 클래스의 성능 향상

기본적으로 클래스들은 sealed 속성을 가지고 있지 않다.

이는 누구나 해당 클래스를 상속할 수 있음을 의미한다.

필자가 생각하기에 올바른 기조는 아닌 듯 하다.

클래스가 애초부터 상속을 해주지 않을 목적으로 설계되었다면(끝 단 파생 클래스로써 설계되었다면)

이는 sealed 속성을 갖추고 있어야 한다.

기본적으로 sealed 속성을 갖추고 있다고 하더라도

나중에 언제든지 이 속성을 제거할 수 있다는 점에서 전혀 디메리트가 없고,

추가적으로, 퍼포먼스에서 이점을 가져갈 수 있다.

 

* .NET 7 부터 sealed 속성을 붙일 수 있는 클래스들을 탐색해주는 새로운 analyzer를 사용할 수 있다.

 

# 퍼포먼스 향상

## 가상 함수 호출

가상 함수를 호출할 때,

실제 함수는 런타임 시의 해당 타입 객체에 존재한다.

각 타입은 모든 가상 함수들의 주소를 포함하고 있는 가상 테이블을 가지고 있고.

이 포인터들은 런타임에 실행할 대상이 되는 메소드들을 실행하기 위해 사용된다.

 

만약 Just In Time 컴파일러가 어떤 타입의 객체에 액세스해야 하는지를 알고 있다면,

가상 테이블으로의 액세스를 스킵하고, 바로 메소드를 수행하여 퍼포먼스가 향상될 수 있다.

 

sealed 속성을 사용하면 다른 파생 클래스가 존재할 수 없으므로

위와 같은 상황을 만드는 데 도움이 된다.

public class SealedBenchmark
{
    readonly NonSealedType nonSealedType = new();
    readonly SealedType sealedType = new();

    [Benchmark(Baseline = true)]
    public void NonSealed()
    {
        // The JIT cannot know what is the actual type of nonSealedType
        // because it can be a class inherited from NonSealedType.
        // So it must use a virtual call.
        nonSealedType.Method();
    }

    [Benchmark]
    public void Sealed()
    {
        // The JIT is sure sealedType is a SealedType. As the class is sealed,
        // it cannot be an instance from a derived type.
        // So it can use a direct call which is faster.
        sealedType.Method();
    }
}

internal class BaseType
{
    public virtual void Method() { }
}
internal class NonSealedType : BaseType
{
    public override void Method() { }
}
internal sealed class SealedType : BaseType
{
    public override void Method() { }
}
METHOD MEAN ERROR STDDEV MEDIAN RATIO CODE SIZE
NonSealed 0.4465 ns 0.0276 ns 0.0258 ns 0.4437 ns 1.00 18 B
Sealed 0.0107 ns 0.0160 ns 0.0150 ns 0.0000 ns 0.02 7 B

JIT 컴파일러가 해당 타입을 확실히 판별할 수 있을 때에는

딱히 sealed 속성이 없어도 바로 실제 method를 호출할 수 있다.

예를 들면, 다음 코드와 같은 상황에서는 전혀 성능 상 차이가 없을 것이다.

void NonSealed()
{
    var instance = new NonSealedType();
    instance.Method(); // The JIT knows instance is NonSealedType, so it uses a direct call
}

void Sealed()
{
    var instance = new SealedType();
    instance.Method(); // The JIT knows instance is SealedType, so it uses a direct call
}

 

##객체 캐스팅

객체를 캐스팅할 때, 런타임 시에는 반드시 해당 객체의 타입을 체크해야 한다.

non-sealed 타입으로 캐스팅 할 때에는, 런타임 시 해당 hierarchy의 모든 타입을 체크해야 하지만.

sealed 타입으로 캐스팅 할 때에는 해당 객체의 타입만을 체크하기 때문에, 더 빠르다.

public class SealedBenchmark
{
    readonly BaseType baseType = new();

    [Benchmark(Baseline = true)]
    public bool Is_Sealed() => baseType is SealedType;

    [Benchmark]
    public bool Is_NonSealed() => baseType is NonSealedType;
}

internal class BaseType {}
internal class NonSealedType : BaseType {}
internal sealed class SealedType : BaseType {}
METHOD MEAN ERROR STDDEV RATIO
Is_NonSealed 1.6560 ns 0.0223 ns 0.0208 ns 1.00
Is_Sealed 0.1505 ns 0.0221 ns 0.0207 ns 0.09

 

##배열

.NET에서 배열은 암시적 참조 변환이 가능하다.

예를 들면 다음과 같은 코드는 동작한다.

BaseType[] value = new DerivedType[1];

다른 collection에는 적용되지 않는다. 

다음과 같은 코드는 동작하지 않는다.

List<BaseType> value = new List<DerivedType>();

 

암시적 참조 변환은 퍼포먼스 저하로 이어진다. 

JIT 컴파일러가 해당 타입을 배열에 할당하기 전에 타입에 대한 체크를 해야하기 때문이다.

sealed 속성을 사용한다면, JIT 컴파일러가 위의 체크 과정을 생략할 수 있다.

public class SealedBenchmark
{
    SealedType[] sealedTypeArray = new SealedType[100];
    NonSealedType[] nonSealedTypeArray = new NonSealedType[100];

    [Benchmark(Baseline = true)]
    public void NonSealed()
    {
        nonSealedTypeArray[0] = new NonSealedType();
    }

    [Benchmark]
    public void Sealed()
    {
        sealedTypeArray[0] = new SealedType();
    }

}

internal class BaseType { }
internal class NonSealedType : BaseType { }
internal sealed class SealedType : BaseType { }

 

METHOD MEAN ERROR STDDEV RATIO CODE SIZE
NonSealed 3.420 ns 0.0897 ns 0.0881 ns 1.00 44 B
Sealed 2.951 ns 0.0781 ns 0.0802 ns 0.86  58 B

 

##배열을 Span<T>로 변환

위의 배열 항목과 같은 이유로, 배열을 Span<T> 또는 ReadOnlySpan<T>로 변환하는 경우에도

똑같은 현상이 발생한다. 이 또한 차이가 크지는 않지만. 퍼포먼스의 향상으로 이어진다.

public class SealedBenchmark
{
    SealedType[] sealedTypeArray = new SealedType[100];
    NonSealedType[] nonSealedTypeArray = new NonSealedType[100];

    [Benchmark(Baseline = true)]
    public Span<NonSealedType> NonSealed() => nonSealedTypeArray;

    [Benchmark]
    public Span<SealedType> Sealed() => sealedTypeArray;
}

public class BaseType {}
public class NonSealedType : BaseType { }
public sealed class SealedType : BaseType { }
METHOD MEAN ERROR STDDEV RATIO CODE SIZE
NonSealed 0.0668 ns 0.0156 ns 0.0138 ns 1.00 64 B
Sealed 0.0307 ns 0.0209 ns 0.0185 ns 0.50  35 B

 

##부적절한 코드 검출

sealed 속성을 사용할 때, 컴파일러는 특정 타입 변환이 불가능함을 알 수 있다.

컴파일러가 warning과 error를 미리 띄워줄 수 있으므로,

부적절한 코드를 미리 제거할 수 있다.

class Sample
{
    public void Foo(NonSealedType obj)
    {
        _ = obj as IMyInterface; // ok because a derived class can implement the interface
    }

    public void Foo(SealedType obj)
    {
        _ = obj is IMyInterface; // ⚠️ Warning CS0184
        _ = obj as IMyInterface; // ❌ Error CS0039
    }
}

public class NonSealedType { }
public sealed class SealedType { }
public interface IMyInterface { }