봉인 된 수업은 실제로 성과 이점을 제공합니까?


140

추가 성능 이점을 얻으려면 클래스를 봉인으로 표시해야한다고 말하는 많은 최적화 팁을 보았습니다.

성능 차이를 확인하기 위해 몇 가지 테스트를 실행했지만 아무것도 발견하지 못했습니다. 내가 뭔가 잘못하고 있습니까? 봉인 클래스가 더 나은 결과를 제공하는 사례를 놓치고 있습니까?

누구든지 테스트를 실행하고 차이를 보았습니까?

배우도록 도와주세요 :)


9
나는 봉인 된 수업이 퍼포먼스를 높이기위한 것이 아니라고 생각합니다. 그들이하는 사실은 부수적 일 수 있습니다. 또한 봉인 클래스를 사용하도록 앱을 리팩토링 한 후 앱을 프로파일 링하고 그 가치가 있는지 판단하십시오. 불필요한 미세 최적화를 위해 확장 성을 잠그면 장기적으로 비용이 발생합니다. 물론, 프로파일을 작성하고 성능을 위해 성능이 아닌 성능 벤치 마크를 달성 할 수 있다면, 지출 한 돈이 가치가 있다면 팀으로서 결정을 내릴 수 있습니다. 당신은 비 반환 한 이유로 클래스를 밀봉 한 경우, 그들을 :) 유지
Merlyn 모건 - 그레이엄

1
성찰 해 보셨습니까? 난 반사에 의해 인스턴스화하는 봉인 클래스와 빠른 어딘가에 읽기
ONOF

내 지식으로는 아무것도 없습니다. 다른 이유가 있습니다-확장 성을 차단하기 위해 많은 경우에 유용하고 필요할 수 있습니다. 여기서 성능 최적화는 목표가 아니 었습니다.
TomTom

...하지만 컴파일러에 대해 생각한다면 : 클래스가 봉인되어 있으면 컴파일 타임에 클래스에서 호출하는 메소드의 주소를 알 수 있습니다. 클래스가 봉인되지 않은 경우 재정의를 호출해야 할 수 있으므로 런타임에 메서드를 해결해야합니다. 확실히 무시할 수 있지만 약간의 차이 가 있음을 알 수 있습니다 .
Dan Puzey

그렇습니다. 그러나 OP가 지적했듯이 이러한 종류의 혜택은 실질적인 이점으로 해석되지 않습니다. 구조적 차이는 훨씬 더 관련이있을 수 있습니다.
TomTom

답변:


60

JITter는 더 이상 확장 할 수있는 방법이 없기 때문에 봉인 된 클래스의 메소드에 대한 비가 상 호출을 사용하기도합니다.

전화 유형, 가상 / 비가 상 전화에 관한 복잡한 규칙이 있으며, 나는 그것들을 모두 알지 못하므로 실제로 당신을 위해 그것을 설명 할 수는 없지만 봉인 클래스와 가상 메소드를 구글로 검색하면 주제에 대한 기사를 찾을 수 있습니다.

이 수준의 최적화를 통해 얻을 수있는 모든 종류의 성능 이점은 최후의 수단으로 간주해야하며 코드 수준에서 최적화하기 전에 항상 알고리즘 수준에서 최적화해야합니다.

다음은이를 언급하는 링크 입니다. 봉인 된 키워드에 대한 램 블링


2
'램 블링'링크는 기술적으로 좋은 것처럼 들리지만 실제로는 말도 안된다는 점에서 흥미 롭습니다. 자세한 내용은 기사에 대한 의견을 읽으십시오. 요약 : - [다음 주석 참조] 주어진 3 가지 이유는 버전 관리, 성능 및 보안 / 예측 가능성이다
스티븐 A. 로우

1
[계속] 버전 관리는 서브 클래스가없는 경우에만 적용됩니다. 그러나이 인수를 모든 클래스로 확장하면 갑자기 상속이없고 언어가 더 이상 객체 지향적이지 않습니다 (단지 객체 기반). [다음 참조]
Steven A. Lowe

3
성능 예는 농담이다 : 가상 메소드 호출을 최적화하는 것; 봉인 된 클래스가 서브 클래스 화 될 수 없기 때문에 처음에 가상 메소드를 갖는 이유는 무엇입니까? 마지막으로, 보안 / 예측 가능성 인수는 분명히 치명적입니다. '사용할 수 없으므로 안전하고 예측 가능합니다.' 롤!
Steven A. Lowe

16
@Steven A. Lowe-Jeffrey Richter가 약간 우회적 인 방식으로 말하려고 시도한 것은 클래스를 개봉하지 않은 경우 파생 클래스가 클래스를 사용하거나 사용하지 않는 방법에 대해 생각해야한다는 것입니다. 이 작업을 올바르게 수행하려면 시간이나 성향을 잃은 다음 나중에 다른 사람의 코드를 변경하지 않을 가능성이 있으므로 봉인하십시오. 그것은 전혀 말도 안되는 것이지, 좋은 상식입니다.
Greg Beech

6
봉인 클래스는이를 선언하는 클래스에서 파생 될 수 있으므로 가상 메서드를 가질 수 있습니다. 그런 다음 나중에 봉인 된 자손 클래스의 변수를 선언하고 해당 메서드를 호출하면 컴파일러는 알려진 구현과 직접적으로 호출 할 수 있습니다. 알려진 구현과 다를 방법이 없기 때문입니다. 해당 클래스의 컴파일 타임 vtable. 봉인 / 봉인되지 않은 것에 대해서는 다른 토론이며, 수업이 기본적으로 봉인 된 이유에 동의합니다.
Lasse V. Karlsen

143

답은 아닙니다. 봉인 된 수업은 봉인되지 않은 수업보다 더 잘 수행되지 않습니다.

이 문제는 callvs callvirtop op 코드와 관련이 있습니다. Call이다보다 빨리 callvirt, 그리고 callvirt객체가 서브 클래 싱 된 경우 당신이 모르는 경우에 주로 사용된다. 따라서 사람들은 클래스를 봉인하면 모든 op 코드가에서 calvirts로 바뀌고 calls더 빠를 것이라고 가정합니다.

불행히도 callvirtnull 참조 확인과 같이 유용하게 사용되는 다른 작업을 수행합니다. 이것은 클래스가 봉인 되더라도 참조가 여전히 널이어서 a callvirt가 필요하다는 것을 의미합니다. 클래스를 봉인 할 필요없이이 문제를 해결할 수 있지만 조금 의미가 없습니다.

Structs call는 서브 클래 싱 할 수없고 null이 아니므로 사용 합니다.

자세한 내용은이 질문을 참조하십시오.

전화 및 콜버트


5
AFAIK, 사용되는 상황 call은 다음과 같습니다. 상황 new T().Method(), struct메소드, 비가 상 virtual메소드 (예 :) 호출 base.Virtual()또는 static메소드. 다른 곳에서는를 사용합니다 callvirt.
porges

1
Uhh ... 이것이 오래되었다는 것을 알고 있지만 이것은 옳지 않습니다 ... 봉인 된 클래스의 큰 승리는 JIT Optimizer가 호출을 인라인 할 때입니다 ...이 경우 봉인 된 클래스는 큰 승리가 될 수 있습니다. .
Brian Kennedy

5
왜이 대답이 틀렸습니까? Mono changelog에서 : "밀봉 된 클래스 및 메소드에 대한 가상화 최적화, IronPython 2.0 pystone 성능을 4 % 향상 시켰습니다. 다른 프로그램에서도 유사한 개선을 기대할 수 있습니다 [Rodrigo]." 봉인 클래스는 성능을 향상시킬 수 있지만 항상 상황에 따라 다릅니다.
Smilediver

1
@Smilediver 성능을 향상시킬 수는 있지만 나쁜 JIT가있는 경우에만 가능합니다 (현재 .NET JIT가 얼마나 좋은지 전혀 모릅니다). 예를 들어 핫스팟은 가상 호출을 인라인하고 필요할 경우 나중에 최적화를 해제합니다. 따라서 실제로 클래스를 서브 클래 싱하는 경우에만 추가 오버 헤드를 지불해야합니다 (필요하지는 않지만).
Voo

1
-1 JIT가 반드시 동일한 IL opcode에 대해 동일한 머신 코드를 생성 할 필요는 없습니다. 널 확인 및 가상 통화는 직교적이고 별도의 callvirt 단계입니다. 봉인 된 유형의 경우 JIT 컴파일러는 여전히 callvirt의 일부를 최적화 할 수 있습니다. JIT 컴파일러가 참조가 널이 아님을 보장 할 수있는 경우에도 마찬가지입니다.
rightfold

29

업데이트 : .NET Core 2.0 및 .NET Desktop 4.7.1부터 CLR은 이제 가상화를 지원합니다. 봉인 클래스의 메소드를 사용하고 가상 호출을 직접 호출로 대체 할 수 있으며, 안전한지 알아낼 수있는 경우 봉인되지 않은 클래스에 대해서도이를 수행 할 수 있습니다.

이 경우 (CLR이 달리 안전하다고 생각할 수없는 봉인 클래스) 봉인 클래스는 실제로 일종의 성능 이점을 제공해야합니다.

즉, 이미 코드를 프로파일 링하고 특히 수백만 번 호출되는 핫 경로 또는 그와 같은 것으로 결정 되지 않은 경우 걱정할 가치가 없다고 생각합니다 .

https://blogs.msdn.microsoft.com/dotnet/2017/06/29/performance-improvements-in-ryujit-in-net-core-and-net-framework/


원래 답변 :

다음 테스트 프로그램을 만든 다음 Reflector를 사용하여 디 컴파일하여 어떤 MSIL 코드가 방출되었는지 확인했습니다.

public class NormalClass {
    public void WriteIt(string x) {
        Console.WriteLine("NormalClass");
        Console.WriteLine(x);
    }
}

public sealed class SealedClass {
    public void WriteIt(string x) {
        Console.WriteLine("SealedClass");
        Console.WriteLine(x);
    }
}

public static void CallNormal() {
    var n = new NormalClass();
    n.WriteIt("a string");
}

public static void CallSealed() {
    var n = new SealedClass();
    n.WriteIt("a string");
}

모든 경우에 C # 컴파일러 (릴리스 빌드 구성의 Visual Studio 2010)는 동일한 MSIL을 내 보내며 다음과 같습니다.

L_0000: newobj instance void <NormalClass or SealedClass>::.ctor()
L_0005: stloc.0 
L_0006: ldloc.0 
L_0007: ldstr "a string"
L_000c: callvirt instance void <NormalClass or SealedClass>::WriteIt(string)
L_0011: ret 

사람들이 봉인이 성능상의 이점을 제공한다고 종종 언급하는 이유는 컴파일러가 클래스가 재정의되지 않는다는 것을 알고 있기 때문에 가상 등을 확인할 필요가 없으므로 call대신 사용할 수 callvirt있기 때문입니다. 진실.

내 생각은 MSIL이 동일하더라도 JIT 컴파일러가 봉인 클래스를 다르게 처리한다는 것입니다.

Visual Studio 디버거에서 릴리스 빌드를 실행하고 디 컴파일 된 x86 출력을 확인했습니다. 두 경우 모두 클래스 이름과 함수 메모리 주소 (물론 달라야 함)를 제외하고 x86 코드는 동일합니다. 여기있어

//            var n = new NormalClass();
00000000  push        ebp 
00000001  mov         ebp,esp 
00000003  sub         esp,8 
00000006  cmp         dword ptr ds:[00585314h],0 
0000000d  je          00000014 
0000000f  call        70032C33 
00000014  xor         edx,edx 
00000016  mov         dword ptr [ebp-4],edx 
00000019  mov         ecx,588230h 
0000001e  call        FFEEEBC0 
00000023  mov         dword ptr [ebp-8],eax 
00000026  mov         ecx,dword ptr [ebp-8] 
00000029  call        dword ptr ds:[00588260h] 
0000002f  mov         eax,dword ptr [ebp-8] 
00000032  mov         dword ptr [ebp-4],eax 
//            n.WriteIt("a string");
00000035  mov         edx,dword ptr ds:[033220DCh] 
0000003b  mov         ecx,dword ptr [ebp-4] 
0000003e  cmp         dword ptr [ecx],ecx 
00000040  call        dword ptr ds:[0058827Ch] 
//        }
00000046  nop 
00000047  mov         esp,ebp 
00000049  pop         ebp 
0000004a  ret 

그런 다음 디버거에서 실행하면 덜 공격적인 최적화를 수행한다고 생각 했습니까?

그런 다음 디버깅 환경 외부에서 독립 실행 형 릴리스 빌드 실행 파일을 실행하고 프로그램이 완료된 후 WinDBG + SOS를 사용하여 JIT 컴파일 된 x86 코드의 해산을 확인했습니다.

아래 코드에서 볼 수 있듯이 디버거 외부에서 실행할 때 JIT 컴파일러가 더 공격적이며 WriteIt메서드를 호출자에게 바로 인라인했습니다 . 그러나 중요한 것은 봉인 된 클래스와 봉인되지 않은 클래스를 호출 할 때 동일하다는 것입니다. 봉인 된 클래스와 봉인되지 않은 클래스 간에는 차이가 없습니다.

다음은 일반 클래스를 호출 할 때입니다.

Normal JIT generated code
Begin 003c00b0, size 39
003c00b0 55              push    ebp
003c00b1 8bec            mov     ebp,esp
003c00b3 b994391800      mov     ecx,183994h (MT: ScratchConsoleApplicationFX4.NormalClass)
003c00b8 e8631fdbff      call    00172020 (JitHelp: CORINFO_HELP_NEWSFAST)
003c00bd e80e70106f      call    mscorlib_ni+0x2570d0 (6f4c70d0) (System.Console.get_Out(), mdToken: 060008fd)
003c00c2 8bc8            mov     ecx,eax
003c00c4 8b1530203003    mov     edx,dword ptr ds:[3302030h] ("NormalClass")
003c00ca 8b01            mov     eax,dword ptr [ecx]
003c00cc 8b403c          mov     eax,dword ptr [eax+3Ch]
003c00cf ff5010          call    dword ptr [eax+10h]
003c00d2 e8f96f106f      call    mscorlib_ni+0x2570d0 (6f4c70d0) (System.Console.get_Out(), mdToken: 060008fd)
003c00d7 8bc8            mov     ecx,eax
003c00d9 8b1534203003    mov     edx,dword ptr ds:[3302034h] ("a string")
003c00df 8b01            mov     eax,dword ptr [ecx]
003c00e1 8b403c          mov     eax,dword ptr [eax+3Ch]
003c00e4 ff5010          call    dword ptr [eax+10h]
003c00e7 5d              pop     ebp
003c00e8 c3              ret

봉인 클래스 대 :

Normal JIT generated code
Begin 003c0100, size 39
003c0100 55              push    ebp
003c0101 8bec            mov     ebp,esp
003c0103 b90c3a1800      mov     ecx,183A0Ch (MT: ScratchConsoleApplicationFX4.SealedClass)
003c0108 e8131fdbff      call    00172020 (JitHelp: CORINFO_HELP_NEWSFAST)
003c010d e8be6f106f      call    mscorlib_ni+0x2570d0 (6f4c70d0) (System.Console.get_Out(), mdToken: 060008fd)
003c0112 8bc8            mov     ecx,eax
003c0114 8b1538203003    mov     edx,dword ptr ds:[3302038h] ("SealedClass")
003c011a 8b01            mov     eax,dword ptr [ecx]
003c011c 8b403c          mov     eax,dword ptr [eax+3Ch]
003c011f ff5010          call    dword ptr [eax+10h]
003c0122 e8a96f106f      call    mscorlib_ni+0x2570d0 (6f4c70d0) (System.Console.get_Out(), mdToken: 060008fd)
003c0127 8bc8            mov     ecx,eax
003c0129 8b1534203003    mov     edx,dword ptr ds:[3302034h] ("a string")
003c012f 8b01            mov     eax,dword ptr [ecx]
003c0131 8b403c          mov     eax,dword ptr [eax+3Ch]
003c0134 ff5010          call    dword ptr [eax+10h]
003c0137 5d              pop     ebp
003c0138 c3              ret

나에게 이것은 밀봉 클래스와 비밀 봉 클래스에서 메소드를 호출하는 사이에 성능 향상이 불가능 하다는 확실한 증거를 제공합니다 ... 지금은 행복하다고 생각합니다 :-)


다른 답변을 복제하지 않고이 답변을 두 번 게시 한 이유는 무엇입니까? 이것은 내 영역이 아니지만 유효한 복제본처럼 보입니다.
Flexo

1
인라인을 피하고 어떤 호출 작업이 사용되는지 확인하기 위해 메소드가 더 길면 (32 바이트 이상의 IL 코드) 어떻게됩니까? 인라인 된 경우 통화가 표시되지 않으므로 효과를 판단 할 수 없습니다.
ygoe 2016 년

혼란 스럽습니다. callvirt이러한 방법이 가상적이지 않은 이유는 무엇입니까?
괴물

@freakish 나는 이것을 어디에서 보았는지 기억할 수 없지만 CLR callvirt은 봉인 된 메소드에 사용되었다는 것을 읽었다 . 메서드 호출을 호출하기 전에 객체를 null 검사해야하기 때문에 인수를 고려하면 사용하십시오 callvirt. 를 제거 callvirt하고 바로 점프 하려면 ((string)null).methodCall()C ++처럼 허용하도록 C #을 수정해야 하거나 객체가 null이 아니었다는 것을 정적으로 증명해야합니다 (할 수는 있지만 귀찮게하지는 않았습니다)
오리온 에드워즈

1
기계 코드 수준까지 파고의 노력에 가기위한 소품,하지만 될 매우 '이 존재한다는 고체 증거 제공과 같은주의 결정 문 할 수 있는 성능 향상이 될이'. 당신이 보여준 것은 하나의 특정 시나리오에 대해 기본 출력에 차이가 없다는 것입니다. 하나의 데이터 포인트이며 모든 시나리오에 대해 일반화한다고 가정 할 수는 없습니다. 우선, 클래스는 가상 메소드를 정의하지 않으므로 가상 호출이 전혀 필요하지 않습니다.
매트 크레이그

24

아시다시피 성능 이점을 보장 할 수는 없습니다. 그러나 밀폐 된 방법을 사용하면 특정 조건에서 성능 저하를 줄일 수 있습니다. (밀봉 클래스는 모든 메소드를 봉인합니다.)

그러나 컴파일러 구현 및 실행 환경에 달려 있습니다.


세부

현대의 많은 CPU는 긴 파이프 라인 구조를 사용하여 성능을 향상시킵니다. CPU는 메모리보다 엄청나게 빠르기 때문에 CPU는 파이프 라인을 가속화하기 위해 메모리에서 코드를 프리 페치해야합니다. 코드가 적절한 시간에 준비되지 않으면 파이프 라인이 유휴 상태가됩니다.

이 '프리 페치'최적화를 방해하는 동적 디스패치 라는 큰 장애물 이 있습니다. 이것을 조건부 분기로 이해할 수 있습니다.

// Value of `v` is unknown,
// and can be resolved only at runtime.
// CPU cannot know which code to prefetch.
// Therefore, just prefetch any one of a() or b().
// This is *speculative execution*.
int v = random();
if (v==1) a();
else b();

조건이 해결 될 때까지 다음 코드 위치를 알 수 없으므로 CPU는이 경우 실행할 다음 코드를 프리 페치 할 수 없습니다. 이 그래서 만드는 위험이 파이프 라인 유휴됩니다. 그리고 유휴에 의한 성능 저하는 정기적으로 엄청납니다.

메서드 재정의의 경우에도 비슷한 일이 발생합니다. 컴파일러는 현재 메서드 호출에 대해 적절한 메서드 재정의를 결정할 수 있지만 때로는 불가능합니다. 이 경우 런타임시에만 적절한 방법을 결정할 수 있습니다. 이는 동적 디스패치의 경우이기도하며 동적 유형 언어의 주된 이유는 일반적으로 정적 유형 언어보다 느립니다.

일부 인텔 CPU (최근 인텔 x86 칩 포함)는 상황에 따라 파이프 라인을 활용하기 위해 추론 적 실행 이라는 기술 을 사용합니다. 실행 경로 중 하나를 프리 페치하십시오. 그러나이 기술의 적중률은 그리 높지 않습니다. 그리고 투기 실패로 인해 파이프 라인이 중단되어 성능이 크게 저하됩니다. (이것은 CPU 구현에 의한 것입니다. 일부 모바일 CPU는 에너지 절약을 위해 이러한 종류의 최적화가 아닌 것으로 알려져 있습니다)

기본적으로 C #은 정적으로 컴파일 된 언어입니다. 그러나 항상 그런 것은 아닙니다. 나는 정확한 조건을 모른다. 그리고 이것은 전적으로 컴파일러 구현에 달려있다. 일부 컴파일러는 메서드가로 표시된 경우 메서드 재정의를 방지하여 동적 디스패치 가능성을 제거 할 수 있습니다 sealed. 어리석은 컴파일러는 그렇지 않을 수 있습니다. 이것이의 성능 이점입니다 sealed.


이 답변 ( 정렬되지 않은 배열보다 정렬 된 배열을 처리하는 것이 왜 더 빠릅니까? )은 분기 예측을 훨씬 잘 설명합니다.


1
Pentium 클래스 CPU는 간접 디스패치를 ​​직접 프리 페치합니다. 때로는 함수 포인터 리디렉션이 이런 이유로 if (unguessable)보다 빠릅니다.
Joshua

2
비가 상 또는 봉인 기능의 한 가지 장점은 더 많은 상황에서 인라인 될 수 있다는 것입니다.
코드 InChaos

4

<주제 이외의 범위>

나는 봉인 된 수업을 싫어 합니다. 성능상의 이점이 놀랍더라도 (물론 의심 스럽지만) 상속을 통한 재사용을 방지하여 객체 지향 모델 을 파괴 합니다. 예를 들어 Thread 클래스는 봉인되어 있습니다. 스레드가 가능한 효율적 이길 원한다는 것을 알 수 있지만 스레드를 서브 클래 싱 할 수 있으면 큰 이점이있는 시나리오도 상상할 수 있습니다. 수업 저자 는 "성능"이유로 수업을 봉인 해야하는 경우 최소한 인터페이스를 제공하여 잊어 버린 기능이 필요한 모든 곳에서 랩핑 및 교체 할 필요가 없도록하십시오.

예 : Thread가 봉인되고 IThread 인터페이스가 없기 때문에 SafeThread 는 Thread 클래스를 래핑해야했습니다. SafeThread는 스레드에서 처리되지 않은 예외를 자동으로 트랩합니다. 이는 스레드 클래스에서 완전히 누락 된 것입니다. 또한 처리되지 않은 예외 이벤트는 보조 스레드에서 처리되지 않은 예외를 선택 하지 않습니다 .

</ off-topic-rant>


35
나는 성능상의 이유로 수업을 봉인하지 않습니다. 디자인상의 이유로 봉인합니다. 상속을위한 디자인은 어렵고 그 노력은 대부분 낭비 될 것입니다. 나는 인터페이스를 제공하는 것에 전적으로 동의합니다-그것은 봉인 해제 클래스에 대한 훨씬 우수한 솔루션입니다.
Jon Skeet

6
캡슐화는 일반적으로 상속보다 더 나은 솔루션입니다. 특정 스레드 예를 들어, 스레드 예외를 트래핑하면 스레드 클래스의 문서화 된 동작을 변경했기 때문에 Liskov 대체 원칙이 깨지므로,이를 파생시킬 수 있어도 어디서나 SafeThread를 사용할 수 있다고 말하는 것은 합리적이지 않습니다. Thread를 사용할 수 있습니다. 이 경우 스레드가 다른 문서화 된 동작을 가진 다른 클래스로 캡슐화하는 것이 좋습니다. 때로는 물건이 자신의 이익을 위해 인봉됩니다.
Greg Beech

1
@ [Greg Beech] : 의견이 아닌 사실-Thread에서 상속하여 디자인에 대한 철저한 감독을 고치는 것은 나쁘지 않습니다 .-) 그리고 당신이 LSP를 과대 평가하고 있다고 생각합니다. 이 경우가 아닌 "바람직한 속성을":-)입니다 '처리되지 않은 예외가 프로그램 파괴'이다
스티븐 A. 로우

1
아니요,하지만 다른 개발자가 봉인하지 않거나 코너 케이스를 허용하여 다른 개발자가 내 물건을 남용 할 수 있도록 엉터리 코드를 공유했습니다. 요즘 내 코드의 대부분은 어설 션 및 기타 계약 관련 항목입니다. 그리고 나는 이것이 단지 ***의 고통이 될 수 있다는 사실에 대해 매우 개방적입니다.
튜링

2
우리가 여기 호언 장담 주제의 오프하고있는 때문에, 당신처럼 혐오 봉인 클래스를, 나는 혐오 삼켜 예외. 무언가가 가슴에 걸리는 것보다 더 나쁜 것은 없지만 프로그램은 계속됩니다. JavaScript는 내가 가장 좋아하는 것입니다. 일부 코드를 변경하고 갑자기 버튼을 클릭해도 아무런 효과가 없습니다. 큰! ASP.NET과 UpdatePanel은 또 다른 것입니다. 진지하게, 내 버튼 핸들러가 버릴 경우 큰 거래이며 충돌해야하므로 수정해야 할 것이 있음을 알 수 있습니다! 아무 것도하지 않는 버튼은 충돌 화면을 불러오는 버튼보다 쓸모 가 없습니다 !
로마 Starkov

4

클래스를 표시하면 sealed성능에 영향을 미치지 않아야합니다.

opcode 대신 opcode csc를 방출해야하는 경우가 있습니다 . 그러나 그러한 경우는 드물다.callvirtcall

그리고 클래스에 하위 클래스가 아직 없다는 것을 알고 있다면 JIT는 동일한 비 가상 함수 호출을 수행 callvirt할 수 call있어야합니다. 메소드의 구현이 하나만 존재하는 경우 vtable에서 주소를로드 할 필요가 없습니다. 하나의 구현을 직접 호출하면됩니다. JIT는 기능을 인라인 할 수도 있습니다.

서브 클래스 나중에로드되면 JIT는 해당 기계 코드를 버리고 코드를 다시 컴파일하여 실제 가상 호출을 생성해야하기 때문에 JIT 부분에서 약간의 도박 입니다. 내 생각에 이것은 실제로 자주 발생하지 않습니다.

(그렇습니다. VM 디자이너는 이러한 작은 성능의 승리를 적극적으로 추구합니다.)


3

봉인 클래스 성능 향상을 제공 해야 합니다. 봉인 클래스는 파생 될 수 없으므로 모든 가상 구성원을 비가 상 구성원으로 전환 할 수 있습니다.

물론, 우리는 정말 작은 이익을 이야기하고 있습니다. 프로파일 링에서 문제를 나타내지 않는 한 성능 향상을 위해 클래스를 봉인 된 것으로 표시하지 않습니다.


그들은 해야 하지만 그렇지 않은 것처럼 보입니다. CLR에 null이 아닌 참조 유형의 개념이 있다면 컴파일러가 call대신 방출 할 수 있기 때문에 봉인 클래스가 실제로 더 좋습니다 callvirt... 다른 많은 이유로 인해 null이 아닌 참조 유형을 좋아합니다 ... 한숨 :-(
Orion Edwards

3

나는 "봉인 된"클래스를 일반적인 경우로 생각하고 항상 "봉인 된"키워드를 생략 할 이유가 있습니다.

가장 중요한 이유는 다음과 같습니다.

a) 더 나은 컴파일 시간 검사 (구현되지 않은 인터페이스에 대한 캐스팅은 런타임뿐만 아니라 컴파일 시간에 감지됩니다)

그리고 가장 큰 이유 :

b) 내 수업의 남용은 그런 식으로 불가능합니다

마이크로 소프트가 "밀봉되지 않은"표준이 아닌 "밀봉 된"표준을 만들었기를 바란다.


나는 "생략"(생각)이 "생략"(생성)이어야한다고 생각합니까?
user2864740

2

@Vaibhav, 성능을 측정하기 위해 어떤 종류의 테스트를 실행 했습니까?

Rotor 를 사용 하고 CLI를 뚫고 봉인 클래스가 성능을 향상시키는 방법을 이해해야한다고 생각합니다.

SSCLI (로터)
SSCLI : 공유 소스 공용 언어 인프라

CLI (공용 언어 인프라)는 .NET Framework의 핵심을 설명하는 ECMA 표준입니다. Rotor라고도하는 SSCLI (공유 소스 CLI)는 Microsoft .NET 아키텍처의 핵심 인 ECMA CLI 및 ECMA C # 언어 사양의 실제 구현에 대한 소스 코드의 압축 된 아카이브입니다.


테스트에는 더미 작업을 수행하는 일부 메소드 (대부분 문자열 조작)를 사용하여 클래스 계층 구조 작성이 포함되었습니다. 이러한 방법 중 일부는 가상이었습니다. 그들은 여기저기서 서로를 부르고있었습니다. 그런 다음이 메소드를 100, 10000 및 100000 회 호출하고 경과 시간을 측정하십시오. 그런 다음 클래스를 봉인 된 것으로 표시 한 후 실행하십시오. 다시 측정합니다. 그들에 차이가 없습니다.
Vaibhav

2

JIT Optimizer가 가상 호출이었던 호출을 인라인 할 수있는 경우 봉인 된 클래스는 조금 더 빠를 수 있지만 때로는 waayyy가 더 빠를 수 있습니다. 따라서 인라인하기에 충분히 작은 메서드라고 불리는 클래스가있는 경우 클래스를 봉인하는 것이 좋습니다.

그러나 수업을 봉인하는 가장 좋은 이유는 "이것을 상속 받도록 설계하지 않았기 때문에 그렇게 설계된 것으로 가정하여 화상을 입지 않도록 할 것입니다. 구현에서 멈춰서 자신을 태워 버릴 것입니다. "

나는 여기에서 어떤 사람들이 무언가에서 파생 될 수있는 기회를 원하기 때문에 봉인 된 수업을 싫어한다고 말했음을 알고 있습니다 ...하지만 가장 유지하기 쉬운 선택은 아닙니다 ... 그. "개인 멤버가있는 수업을 싫어합니다. 액세스 권한이 없기 때문에 원하는 수업을 할 수없는 경우가 많습니다."라고 말하는 것과 비슷합니다. 캡슐화가 중요합니다 ... 밀봉은 캡슐화의 한 형태입니다.


C # 컴파일러는 여전히 callvirt씰링 된 클래스의 메서드에 대해 (가상 메서드 호출)을 사용합니다.이 메서드는 여전히 null 개체 검사를 수행해야하기 때문입니다. 인라인과 관련하여 CLR JIT는 봉인 된 클래스와 봉인되지 않은 클래스 모두에 대한 인라인 가상 메소드 호출을 수행 할 수 있습니다 (그렇습니다). 성능은 신화입니다.
Orion Edwards

1

그것들을 실제로 보려면 JIT 컴파일 된 코드 (마지막) 를 분석해야합니다 .

C # 코드

public sealed class Sealed
{
    public string Message { get; set; }
    public void DoStuff() { }
}
public class Derived : Base
{
    public sealed override void DoStuff() { }
}
public class Base
{
    public string Message { get; set; }
    public virtual void DoStuff() { }
}
static void Main()
{
    Sealed sealedClass = new Sealed();
    sealedClass.DoStuff();
    Derived derivedClass = new Derived();
    derivedClass.DoStuff();
    Base BaseClass = new Base();
    BaseClass.DoStuff();
}

MIL 코드

.method private hidebysig static void  Main() cil managed
{
  .entrypoint
  // Code size       41 (0x29)
  .maxstack  8
  IL_0000:  newobj     instance void ConsoleApp1.Program/Sealed::.ctor()
  IL_0005:  callvirt   instance void ConsoleApp1.Program/Sealed::DoStuff()
  IL_000a:  newobj     instance void ConsoleApp1.Program/Derived::.ctor()
  IL_000f:  callvirt   instance void ConsoleApp1.Program/Base::DoStuff()
  IL_0014:  newobj     instance void ConsoleApp1.Program/Base::.ctor()
  IL_0019:  callvirt   instance void ConsoleApp1.Program/Base::DoStuff()
  IL_0028:  ret
} // end of method Program::Main

JIT 컴파일 된 코드

--- C:\Users\Ivan Porta\source\repos\ConsoleApp1\Program.cs --------------------
        {
0066084A  in          al,dx  
0066084B  push        edi  
0066084C  push        esi  
0066084D  push        ebx  
0066084E  sub         esp,4Ch  
00660851  lea         edi,[ebp-58h]  
00660854  mov         ecx,13h  
00660859  xor         eax,eax  
0066085B  rep stos    dword ptr es:[edi]  
0066085D  cmp         dword ptr ds:[5842F0h],0  
00660864  je          0066086B  
00660866  call        744CFAD0  
0066086B  xor         edx,edx  
0066086D  mov         dword ptr [ebp-3Ch],edx  
00660870  xor         edx,edx  
00660872  mov         dword ptr [ebp-48h],edx  
00660875  xor         edx,edx  
00660877  mov         dword ptr [ebp-44h],edx  
0066087A  xor         edx,edx  
0066087C  mov         dword ptr [ebp-40h],edx  
0066087F  nop  
            Sealed sealedClass = new Sealed();
00660880  mov         ecx,584E1Ch  
00660885  call        005730F4  
0066088A  mov         dword ptr [ebp-4Ch],eax  
0066088D  mov         ecx,dword ptr [ebp-4Ch]  
00660890  call        00660468  
00660895  mov         eax,dword ptr [ebp-4Ch]  
00660898  mov         dword ptr [ebp-3Ch],eax  
            sealedClass.DoStuff();
0066089B  mov         ecx,dword ptr [ebp-3Ch]  
0066089E  cmp         dword ptr [ecx],ecx  
006608A0  call        00660460  
006608A5  nop  
            Derived derivedClass = new Derived();
006608A6  mov         ecx,584F3Ch  
006608AB  call        005730F4  
006608B0  mov         dword ptr [ebp-50h],eax  
006608B3  mov         ecx,dword ptr [ebp-50h]  
006608B6  call        006604A8  
006608BB  mov         eax,dword ptr [ebp-50h]  
006608BE  mov         dword ptr [ebp-40h],eax  
            derivedClass.DoStuff();
006608C1  mov         ecx,dword ptr [ebp-40h]  
006608C4  mov         eax,dword ptr [ecx]  
006608C6  mov         eax,dword ptr [eax+28h]  
006608C9  call        dword ptr [eax+10h]  
006608CC  nop  
            Base BaseClass = new Base();
006608CD  mov         ecx,584EC0h  
006608D2  call        005730F4  
006608D7  mov         dword ptr [ebp-54h],eax  
006608DA  mov         ecx,dword ptr [ebp-54h]  
006608DD  call        00660490  
006608E2  mov         eax,dword ptr [ebp-54h]  
006608E5  mov         dword ptr [ebp-44h],eax  
            BaseClass.DoStuff();
006608E8  mov         ecx,dword ptr [ebp-44h]  
006608EB  mov         eax,dword ptr [ecx]  
006608ED  mov         eax,dword ptr [eax+28h]  
006608F0  call        dword ptr [eax+10h]  
006608F3  nop  
        }
0066091A  nop  
0066091B  lea         esp,[ebp-0Ch]  
0066091E  pop         ebx  
0066091F  pop         esi  
00660920  pop         edi  
00660921  pop         ebp  

00660922  ret  

객체 생성은 동일하지만 봉인 및 파생 / 기본 클래스의 메서드를 호출하기 위해 실행되는 명령은 약간 다릅니다. 데이터를 레지스터 또는 RAM (mov 명령)으로 옮긴 후 봉인 된 메소드를 호출하고 dword ptr [ecx], ecx (cmp 명령) 간의 비교를 수행 한 다음 파생 / 기본 클래스가 메소드를 직접 실행하는 동안 메소드를 호출하십시오. .

Torbj¨orn Granlund ( AMD 및 Intel x86 프로세서의 명령 대기 시간 및 처리량)가 작성한 보고서에 따르면 Intel Pentium 4에서 다음 명령의 속도는 다음과 같습니다.

  • mov : 대기 시간으로 1주기를 가지며 프로세서는이 유형의주기마다 2.5 명령어를 유지할 수 있습니다
  • cmp : 대기 시간으로 1주기를 가지며 프로세서는이 유형의주기마다 2 개의 명령어를 유지할 수 있습니다.

링크 : https://gmplib.org/~tege/x86-timing.pdf

즉, 이상적으로 는 봉인 된 메소드를 호출하는 데 필요한 시간은 2주기이고 파생 또는 기본 클래스 메소드를 호출하는 데 필요한 시간은 3주기입니다.

컴파일러 최적화는 봉인 된 클래스와 봉인되지 않은 클래스의 성능을 너무 낮게 설정하여 프로세서 원에 대해 이야기하고 있으며, 이러한 이유로 대부분의 응용 프로그램과 관련이 없습니다.


-10

이 코드를 실행하면 봉인 클래스가 2 배 빠릅니다.

class Program
{
    static void Main(string[] args)
    {
        Console.ReadLine();

        var watch = new Stopwatch();
        watch.Start();
        for (int i = 0; i < 10000000; i++)
        {
            new SealedClass().GetName();
        }
        watch.Stop();
        Console.WriteLine("Sealed class : {0}", watch.Elapsed.ToString());

        watch.Start();
        for (int i = 0; i < 10000000; i++)
        {
            new NonSealedClass().GetName();
        }
        watch.Stop();
        Console.WriteLine("NonSealed class : {0}", watch.Elapsed.ToString());

        Console.ReadKey();
    }
}

sealed class SealedClass
{
    public string GetName()
    {
        return "SealedClass";
    }
}

class NonSealedClass
{
    public string GetName()
    {
        return "NonSealedClass";
    }
}

출력 : 밀봉 클래스 : 00 : 00 : 00.1897568 비밀 봉 클래스 : 00 : 00 : 00.3826678


9
이것에는 몇 가지 문제가 있습니다. 먼저, 첫 번째 테스트와 두 번째 테스트 사이에서 스톱워치를 재설정하지 않습니다. 둘째, 메소드를 호출하는 방식은 모든 op 코드가 callvirt가 아니라 호출되므로 유형이 중요하지 않음을 의미합니다.
Cameron MacFarland

1
RuslanG, 첫 번째 테스트를 실행 한 후 watch.Reset ()을 호출하는 것을 잊었습니다. o_O :]

예, 위의 코드에는 버그가 있습니다. 그렇다면 다시 누가 코드에 실수를 일으키지 않기 위해 자신에 대해 자랑 할 수 있습니까? 그러나이 답변은 다른 모든 것보다 한 가지 중요한 점이 있습니다. 문제의 영향을 측정하려고합니다. 그리고이 답변은 또한 당신이 검사하고 [mass-] downvote (모든 질량 downvoting이 왜 필요한지 궁금합니다)에 대한 코드를 공유합니다. 다른 답변과 달리. 내 의견으로는 존중받을 가치가 있습니다 ... 또한 사용자는 초보자이므로 매우 환영하는 접근 방식이 아닙니다. 몇 가지 간단한 수정 사항과이 코드는 우리 모두에게 유용합니다. 당신이 감히 경우 수정 + 공유 고정 버전
Roland Pihlakas

@RolandPihlakas에 동의하지 않습니다. 당신이 말했듯이, "코드에 실수를 일으키지 않기 위해 자신에 대해 자랑 할 수있는 사람". 답변-> 아니오, 없음 그러나 그것은 요점이 아닙니다. 요점은 다른 새로운 프로그래머를 오도 할 수있는 잘못된 정보입니다. 많은 사람들이 스톱워치가 재설정되지 않았다는 것을 쉽게 놓칠 수 있습니다. 그들은 벤치 마크 정보가 사실이라고 믿을 수있었습니다. 더 해롭지 않습니까? 답변을 빨리 찾는 사람은 이러한 의견을 읽지 못할 수도 있습니다. 오히려 답변을보고 믿을 수도 있습니다.
Emran Hussain 2:17에
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.