코딩 팁

C# Sealed 클래스는 왜 빠를까? 최적화에 필요한 이유

코딩 기록 vicddory 2017. 3. 5. 12:00

컴파일러는 낮은 레벨에서 C# sealed 클래스를 micro-optimization 할 수 있습니다. 컴파일 타임에 C# sealed 클래스에 함수를 호출하면 callvirt IL 명령어 대신 IL 명령어를 사용합니다.


  1. C# sealed 클래스의 함수는 오버라이드 되지 않음.
  2. 가상 함수 테이블 검사 과정을 생략.
  3. 가상 함수 테이블은 조회 시간이 상대적으로 짦음.
  4. callvirt 보다 속도에서 유리함.


이렇기에 성능이 조금 좋아집니다. 최적화 중이라면 적용하세요.


즉, 다형성을 잃어버린 클래스라 선언했기에 컴파일러는 여러 검사 과정을 생략합니다. (확장되지 않는 클래스)


예를 들어, non-sealed 클래스에 makeNoise()란 함수가 있을 때, 이 함수의 재정의 여부는 알 수가 없습니다.

함수 재정의 여부를 알 수 있을까?


클래스의 인스턴스가 makeNoise()를 호출할 때마다 이 인스턴스의 클래스 계층 구조를 검사해 재정의 함수인지를 확인합니다.


  1. C# sealed 클래스의 함수라면 재정의 할 수 없음.
  2. 컴파일러는 1번 사항을 알고 있음.
  3. 컴파일러는 가상 테이블을 사용하지 않음.
  4. 분기 시점에 사용하는 명령어로 컴파일. (테이블은 놔두고 간략한 명령어를 만들어 사용)


잠재적으로 상속과정에 영향을 미칠 클래스가 아니라면 C# sealed를 이용하는 것이 성능 면에서 조금 유리한 이유입니다. sealed 키워드 사용은 응용 프로그램의 성능을 최적화 / 향상하는 마지막 방법입니다.


C# Sealed 클래스는 왜 빠를까 [Tutorial][C# Tutorial] 성능 향상


아래는 C# 컴파일러가 Call, Callvirt 명령어 사용을 할 때의 예제입니다.



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public sealed class SealedClass
{
    public void DoSmth()
    { }
}
 
public class ClassWithSealedMethod : ClassWithVirtualMethod
{
    public sealed override void DoSmth()
    { }
}
 
public class ClassWithVirtualMethod
{
    public virtual void DoSmth()
    { }
}
cs



위의 DoSmth() 함수 모두를 호출하려면 아래처럼 구성합니다.



1
2
3
4
5
6
7
8
9
10
11
public void Call()
{
    SealedClass sc = new SealedClass();
    sc.DoSmth();
 
    ClassWithVirtualMethod cwcm = new ClassWithVirtualMethod();
    cwcm.DoSmth();
 
    ClassWithSealedMethod cwsm = new ClassWithSealedMethod();
    cwsm.DoSmth();
}
cs



C# 컴파일러는 이론적으로 2번의 callvirt와 1번의 call 명령을 수행해야 합니다. 그러나, 실제론 그렇지 않습니다. (callvirt만 3번)



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
.method public hidebysig instance void Call() cil managed
{
    .maxstack 1
    .locals init (
        [0class TestApp.SealedClasses.SealedClass sc,
        [1class TestApp.SealedClasses.ClassWithVirtualMethod cwcm,
        [2class TestApp.SealedClasses.ClassWithSealedMethod cwsm)
    L_0000: newobj instance void TestApp.SealedClasses.SealedClass::.ctor()
    L_0005: stloc.0 
    L_0006: ldloc.0 
    L_0007: callvirt instance void TestApp.SealedClasses.SealedClass::DoSmth()
    L_000c: newobj instance void TestApp.SealedClasses.ClassWithVirtualMethod::.ctor()
    L_0011: stloc.1 
    L_0012: ldloc.1 
    L_0013: callvirt instance void TestApp.SealedClasses.ClassWithVirtualMethod::DoSmth()
    L_0018: newobj instance void TestApp.SealedClasses.ClassWithSealedMethod::.ctor()
    L_001d: stloc.2 
    L_001e: ldloc.2 
    L_001f: callvirt instance void TestApp.SealedClasses.ClassWithVirtualMethod::DoSmth()
    L_0024: ret 
}
cs



단순한 이유 때문입니다.


런타임 단계에선 DoSmth() 함수를 호출하기 전에 Type 인스턴스의 null 여부를 확인합니다.



1
2
3
4
5
6
7
8
public void Call()
{
    new SealedClass().DoSmth();
 
    new ClassWithVirtualMethod().DoSmth();
 
    new ClassWithSealedMethod().DoSmth();
}
cs

최적화 소스 코드 변경


그래서, C# 컴파일러가 최적화 될 수 있도록 코드를 변경해야 합니다.

아래는 C# sealed 클래스 변형 소스입니다.



1
2
3
4
5
6
7
8
9
10
11
.method public hidebysig instance void Call() cil managed
{
    .maxstack 8
    L_0000: newobj instance void TestApp.SealedClasses.SealedClass::.ctor()
    L_0005: call instance void TestApp.SealedClasses.SealedClass::DoSmth()
    L_000a: newobj instance void TestApp.SealedClasses.ClassWithVirtualMethod::.ctor()
    L_000f: callvirt instance void TestApp.SealedClasses.ClassWithVirtualMethod::DoSmth()
    L_0014: newobj instance void TestApp.SealedClasses.ClassWithSealedMethod::.ctor()
    L_0019: callvirt instance void TestApp.SealedClasses.ClassWithVirtualMethod::DoSmth()
    L_001e: ret 
}
cs



non-sealed 클래스의 non-virtual 함수를 같은 방법으로 호출하면 callvirt 대신 call 명령이 실행됩니다. 출처는 스택오버플로우였습니다.


C# 클래스 sealed 속도[C# Tutorial] 성능 향상


C# 관련 글


속도 개선 9가지 방법

https://codingcoding.tistory.com/838


코드 최적화 7가지

https://codingcoding.tistory.com/330


C# 처리 속도 비교 Tuple vs KeyValuePair

https://codingcoding.tistory.com/206

반응형
그리드형(광고전용)