티스토리 뷰
목차
컴파일러는 낮은 레벨에서 C# sealed 클래스를 micro-optimization 할 수 있습니다. 컴파일 타임에 C# sealed 클래스에 함수를 호출하면 callvirt IL 명령어 대신 IL 명령어를 사용합니다.
- C# sealed 클래스의 함수는 오버라이드 되지 않음.
- 가상 함수 테이블 검사 과정을 생략.
- 가상 함수 테이블은 조회 시간이 상대적으로 짦음.
- callvirt 보다 속도에서 유리함.
이렇기에 성능이 조금 좋아집니다. 최적화 중이라면 적용하세요.
즉, 다형성을 잃어버린 클래스라 선언했기에 컴파일러는 여러 검사 과정을 생략합니다. (확장되지 않는 클래스)
예를 들어, non-sealed 클래스에 makeNoise()란 함수가 있을 때, 이 함수의 재정의 여부는 알 수가 없습니다.
함수 재정의 여부를 알 수 있을까?
클래스의 인스턴스가 makeNoise()를 호출할 때마다 이 인스턴스의 클래스 계층 구조를 검사해 재정의 함수인지를 확인합니다.
- C# sealed 클래스의 함수라면 재정의 할 수 없음.
- 컴파일러는 1번 사항을 알고 있음.
- 컴파일러는 가상 테이블을 사용하지 않음.
- 분기 시점에 사용하는 명령어로 컴파일. (테이블은 놔두고 간략한 명령어를 만들어 사용)
잠재적으로 상속과정에 영향을 미칠 클래스가 아니라면 C# sealed를 이용하는 것이 성능 면에서 조금 유리한 이유입니다. sealed 키워드 사용은 응용 프로그램의 성능을 최적화 / 향상하는 마지막 방법입니다.
[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 ( [0] class TestApp.SealedClasses.SealedClass sc, [1] class TestApp.SealedClasses.ClassWithVirtualMethod cwcm, [2] class 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# Tutorial] 성능 향상
C# 관련 글
속도 개선 9가지 방법
https://codingcoding.tistory.com/838
코드 최적화 7가지
https://codingcoding.tistory.com/330
C# 처리 속도 비교 Tuple vs KeyValuePair