눈팅하는 게임개발자 블로그
[C#] Performance benefits of sealed class in .NET 본문
원문
https://www.meziantou.net/performance-benefits-of-sealed-class.htm
.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 { }
'공부한거 > Unity' 카테고리의 다른 글
Editor Inspector Categorize (0) | 2022.02.26 |
---|---|
Unity ILL2CPP 빌드시 실행직후 바로 크래쉬나는 현상. (0) | 2019.08.11 |