为什么密封类型更快?

时间:2009-05-26 16:54:04

标签: c# .net performance clr

为什么密封类型更快?

我想知道为什么这是真的更深层次的细节。

6 个答案:

答案 0 :(得分:36)

在最低级别,编译器可以在密封类时进行微优化。

如果你在一个密封类上调用一个方法,并且在编译时将该类型声明为该密封类,则编译器可以使用调用IL指令而不是callvirt来实现方法调用(在大多数情况下) IL指令。这是因为无法覆盖方法目标。调用消除了空检查,并且比callvirt执行更快的vtable查找,因为它不必检查虚拟表。

这可以对性能进行非常非常小的改进。

话虽如此,在决定是否密封课程时,我会完全忽略这一点。标记密封的类型确实应该是设计决策,而不是性能决策。您是否希望现在或将来人们(包括您自己)可能从您的班级继承?如果是这样,请不要密封。如果没有,请密封。这确实应该是决定因素。

答案 1 :(得分:10)

基本上,它与他们不必担心虚拟功能表的扩展这一事实有关;密封类型不能扩展,因此,运行时不需要关心它们如何是多态的。

答案 2 :(得分:8)

决定发布小代码示例以说明C#编译器何时发出“call”& “callvirt”说明。

所以,这是我使用的所有类型的源代码:

    public sealed class SealedClass
    {
        public void DoSmth()
        { }
    }

    public class ClassWithSealedMethod : ClassWithVirtualMethod
    {
        public sealed override void DoSmth()
        { }
    }

    public class ClassWithVirtualMethod
    {
        public virtual void DoSmth()
        { }
    }

我还有一个调用所有“DoSmth()”方法的方法:

    public void Call()
    {
        SealedClass sc = new SealedClass();
        sc.DoSmth();

        ClassWithVirtualMethod cwcm = new ClassWithVirtualMethod();
        cwcm.DoSmth();

        ClassWithSealedMethod cwsm = new ClassWithSealedMethod();
        cwsm.DoSmth();
    }

看看“Call()”方法我们可以说(理论上)C#编译器应该发出2个“callvirt”& 1个“通话”说明,对吗? 不幸的是,现实有点不同 - 3“callvirt”-s:

.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 
}

原因很简单:运行时必须在调用“DoSmth()”方法之前检查类型实例是否不等于null。 但是我们仍然可以编写代码,使C#编译器能够发出优化的IL代码:

    public void Call()
    {
        new SealedClass().DoSmth();

        new ClassWithVirtualMethod().DoSmth();

        new ClassWithSealedMethod().DoSmth();
    }

结果是:

.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 
}

如果你尝试以同样的方式调用非密封类的非虚方法,你也会得到“call”指令而不是“callvirt”

答案 3 :(得分:5)

如果JIT编译器使用密封类型看到对虚方法的调用,则可以通过非虚拟地调用该方法来生成更高效的代码。现在调用非虚方法更快,因为不需要执行vtable查找。恕我直言,这是微优化,应该作为提高应用程序性能的最后手段。如果您的方法包含任何代码,则与执行代码本身的成本相比,虚拟版本将比非虚拟版本慢得多。

答案 4 :(得分:3)

为了扩展其他人的答案,无法扩展密封类(相当于Java中的最终类)。这意味着只要编译器看到使用此类的方法,编译器就会完全知道不需要运行时调度。它不必检查类以动态地查看需要调用层次结构中哪个类的方法。这意味着分支可以编译而不是动态。

例如,如果我有一个非密封类Animal,其方法为makeNoise(),则编译器不一定知道任何Animal实例是否覆盖该方法。因此,每次Animal实例调用makeNoise()时,都需要检查实例的类层次结构,以查看实例是否在扩展类中重写此方法。

但是,如果我有一个密封的类AnimalFeeder,它有一个方法feedAnimal(),那么编译器肯定知道这个方法不能被覆盖。它可以在分支中编译为子例程或等效指令,而不是使用虚拟分派表。

注意:您可以在类上使用sealed来阻止从该类继承任何,并且可以对声明为{{1}的方法使用sealed在基类中,以防止进一步覆盖该方法。

答案 5 :(得分:0)

要真正看到它们,您需要分析 JIT编译的编码 e(最后一个)。

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(移动指令)后,调用密封方法,在dword ptr [ecx],ecx(cmp指令)之间执行比较,然后在派生/基类直接执行该方法的同时调用该方法。

根据TorbjörornGranlund撰写的报告, AMD和Intel x86处理器的指令等待时间和吞吐量,Intel Pentium 4中以下指令的速度为:

  • mov :具有1个周期的等待时间,处理器每个此类周期可支持2.5条指令
  • cmp :具有1个周期的延迟,处理器可以在这种类型的每个周期维持2条指令

链接https://gmplib.org/~tege/x86-timing.pdf

这意味着,理想情况下,调用密封方法所需的时间为2个周期,而调用派生或基类方法所需的时间为3个周期。

编译器的优化使密封和未密封的分类之间的性能差异变得如此之低,以至于我们谈论的是处理器界,因此与大多数应用程序无关。