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

컴파일러는 낮은 레벨에서 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

댓글(6)

  • 질문
    2017.07.11 15:26

    안녕하세요. 유익한 내용 감사합니다. 근데 질문이 있어 댓글을 남깁니다.

    첫 문단에서 "sealed 클래스의 함수는 오버라이드 되지 않아 가상 함수 테이블 검사 과정을 생략하기에 callvirt 보다 속도에서 유리한 vtable 조회 과정을 수행합니다. " 라고 했습니다.
    vtable과 가상 테이블이 같은 거라고 알고있는데 다른 개념인 건가요? 만약 같은 거라면 앞에서는 생략한다 나오고 뒤에서는 수행한다고 나와있는데 이 부분이 이해가 안가는데 보충 설명 좀 부탁드려도 될까요?
    또, 네번째 문단에서는 "그렇지만 sealed 클래스의 함수라면 재정의 할 수 없다는 것을 컴파일러는 확실히 알기 때문에 가상 테이블을 사용하지 않고 분기 시점에서 사용하는 명령어로만 컴파일합니다." 라고 했습니다.
    이는 처음 문단의 설명과 다른거 같아 질문드립니다. 'sealed 클래스'라는 같은 대상으로 이해했는데, 왜 다른 동작을 하는지 설명 좀 부탁드립니다. 혹 다른 대상인가요?

    저 두 부분에 대해서 이해가 안되어서 댓글드립니다.ㅠㅠ

    • 2017.07.12 07:50 신고

      제가 읽기 어렵게 적었네요. 수정했으니 다시 읽어주세요.

      그리고, 아래 코드 흐름에도 나오듯 가상 함수 테이블은 사용하질 않아 호출 횟수가 줄어들어 속도에 유리합니다.

  • 질문
    2017.07.12 09:53

    안녕하세요 수정해주신 내용 잘 읽었습니다.
    아직도 이해가 안가는 부분이 있어서 다시 댓글드립니다.

    첫 문단에 callvirt IL 명령어 대신 IL 명령어를 쓴다고 했는데 이는 call명령어를 뜻하는 건가요?

    처음과 두번째 박스는 모두 sealed된 클래스를 실행할 때의 과정같은데 처음은 "3. 가상 함수 테이블은 조회 시간이 상대적으로 짦음."라 되어있고, 두 번째 박스에서는 "3. 컴파일러는 가상 테이블을 사용하지 않음." 라고 되어있습니다. 처음은 가상 함수 테이블을 검사는 안하고 조회한다는거 같고 두번째는 가상 함수 테이블을 검사도 조회하지 않는다는 거 같은데 이 둘의 차이가 뭔지 알고싶습니다.

    • 2017.07.12 11:33 신고

      1. callvirt는 제가 자세히는 모르나 개념적으론 가상 함수 테이블을 호출하는 것으로 알고 있습니다.

      이 부분에 대해선 call, callvirt, new, all 등 ILDASM.exe를 자세히 알아야 합니다.

      저는 MS 개발자가 아니라 진작에 모든 걸 이해하려는 시도는 포기했습니다. ㅎㅎㅎ ^^

      2. 컴파일 단계에서 테이블을 만들지만 그것보단 명령어로 대체한다는 의미입니다.

      비슷한 예가 될지 모르겠지만,

      C++에서 const 키워드가 붙은 변수는 처음부터 변경될 여지가 없으므로 값 변동을 위한 별도 메모리를 할당하지 않습니다.

      비슷한 의미로 변동할 여지가 없으니 테이블을 보고 대치할 명령어를 사용하는 겁니다.

      그게 더 효율적이니깐요. (왜 효율적인진 컴파일러 만든 사람이 잘 알겠죠. 저는
      그렇다고 하니 그런 줄 알고 있음...)


      저는 일단 이렇게 이해했습니다.
      제가 어렵게 이해해서 그런가 글도 어렵게 적었나 봅니다.

    • 질문
      2017.07.12 13:22

      이 부분은 너무 어려운거 같습니다. 그래도
      좀 이해가 되는 것 같습니다.
      답변 감사합니다!

    • 2017.07.12 14:30 신고

      네~ 저도 감사합니다.
      덕분에 저도 머리속이 정리되네요.

      정말 감사합니다.

Designed by JB FACTORY