IT

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

lottoking 2020. 6. 29. 07:38
반응형

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


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

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

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

배우도록 도와주세요 :)


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

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

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

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


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

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

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

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

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

전화 및 콜버트


업데이트 : .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 

사람들이 Sealed가 성능상의 이점을 제공한다고 종종 언급하는 이유는 컴파일러가 클래스가 재정의되지 않는다는 것을 알고 있기 때문에 가상 등을 확인할 필요가 없기 때문에 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

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


As I know, there is no guarantee of performance benefit. But there is a chance to decrease performance penalty under some specific condition with sealed method. (sealed class makes all methods to be sealed.)

But it's up to compiler implementation and execution environment.


Details

Many of modern CPUs use long pipeline structure to increase performance. Because CPU is incredibly faster than memory, CPU has to prefetch code from memory to accelerate pipeline. If the code is not ready at proper time, the pipelines will be idle.

There is a big obstacle called dynamic dispatch which disrupts this 'prefetching' optimization. You can understand this as just a conditional branching.

// 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 cannot prefetch next code to execute in this case because the next code position is unknown until the condition is resolved. So this makes hazard causes pipeline idle. And performance penalty by idle is huge in regular.

Similar thing happen in case of method overriding. Compiler may determine proper method overriding for current method call, but sometimes it's impossible. In this case, proper method can be determined only at runtime. This is also a case of dynamic dispatch, and, a main reason of dynamically-typed languages are generally slower than statically-typed languages.

Some CPU (including recent Intel's x86 chips) uses technique called speculative execution to utilize pipeline even on the situation. Just prefetch one of execution path. But hit rate of this technique is not so high. And speculation failure causes pipeline stall which also makes huge performance penalty. (this is completely by CPU implementation. some mobile CPU is known as does not this kind of optimization to save energy)

Basically, C# is a statically compiled language. But not always. I don't know exact condition and this is entirely up to compiler implementation. Some compilers can eliminate possibility of dynamic dispatch by preventing method overriding if the method is marked as sealed. Stupid compilers may not. This is the performance benefit of the sealed.


This answer (Why is it faster to process a sorted array than an unsorted array?) is describing the branch prediction a lot better.


<off-topic-rant>

I loathe sealed classes. Even if the performance benefits are astounding (which I doubt), they destroy the object-oriented model by preventing reuse via inheritance. For example, the Thread class is sealed. While I can see that one might want threads to be as efficient as possible, I can also imagine scenarios where being able to subclass Thread would have great benefits. Class authors, if you must seal your classes for "performance" reasons, please provide an interface at the very least so we don't have to wrap-and-replace everywhere that we need a feature you forgot.

Example: SafeThread had to wrap the Thread class because Thread is sealed and there is no IThread interface; SafeThread automatically traps unhandled exceptions on threads, something completely missing from the Thread class. [and no, the unhandled exception events do not pick up unhandled exceptions in secondary threads].

</off-topic-rant>


Marking a class sealed should have no performance impact.

There are cases where csc might have to emit a callvirt opcode instead of a call opcode. However, it seems those cases are rare.

And it seems to me that the JIT should be able to emit the same non-virtual function call for callvirt that it would for call, if it knows that the class doesn't have any subclasses (yet). If only one implementation of the method exists, there's no point loading its address from a vtable—just call the one implementation directly. For that matter, the JIT can even inline the function.

It's a bit of a gamble on the JIT's part, because if a subclass is later loaded, the JIT will have to throw away that machine code and compile the code again, emitting a real virtual call. My guess is this doesn't happen often in practice.

(And yes, VM designers really do aggressively pursue these tiny performance wins.)


Sealed classes should provide a performance improvement. Since a sealed class cannot be derived, any virtual members can be turned into non-virtual members.

Of course, we're talking really small gains. I wouldn't mark a class as sealed just to get a performance improvement unless profiling revealed it to be a problem.


I consider "sealed" classes the normal case and I ALWAYS have a reason to omit the "sealed" keyword.

The most important reasons for me are:

a) Better compile time checks (casting to interfaces not implemented will be detected at compile time, not only at runtime)

and, top reason:

b) Abuse of my classes is not possible that way

I wish Microsoft would have made "sealed" the standard, not "unsealed".


@Vaibhav, what kind of tests did you execute to measure performance?

I guess one would have to use Rotor and to drill into CLI and understand how a sealed class would improve performance.

SSCLI (Rotor)
SSCLI: Shared Source Common Language Infrastructure

The Common Language Infrastructure (CLI) is the ECMA standard that describes the core of the .NET Framework. The Shared Source CLI (SSCLI), also known as Rotor, is a compressed archive of the source code to a working implementation of the ECMA CLI and the ECMA C# language specification, technologies at the heart of Microsoft’s .NET architecture.


sealed classes will be at least a tiny bit faster, but sometimes can be waayyy faster... if the JIT Optimizer can inline calls that would have otherwise been virtual calls. So, where there's oft-called methods that are small enough to be inlined, definitely consider sealing the class.

However, the best reason to seal a class is to say "I didn't design this to be inherited from, so I'm not going to let you get burned by assuming it was designed to be so, and I'm not going to burn myself by getting locked into an implementation because I let you derive from it."

I know some here have said they hate sealed classes because they want the opportunity to derive from anything... but that is OFTEN not the most maintainable choice... because exposing a class to derivation locks you in a lot more than not exposing all that. Its similar to saying "I loathe classes that have private members... I often can't make the class do what I want because I don't have access." Encapsulation is important... sealing is one form of encapsulation.


Run this code and you'll see that sealed classes are 2 times faster:

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";
    }
}

output: Sealed class : 00:00:00.1897568 NonSealed class : 00:00:00.3826678

참고URL : https://stackoverflow.com/questions/2134/do-sealed-classes-really-offer-performance-benefits

반응형