为什么密封类型更快?
我想知道为什么这是真的更深层次的细节。
答案 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中以下指令的速度为:
链接:https://gmplib.org/~tege/x86-timing.pdf
这意味着,理想情况下,调用密封方法所需的时间为2个周期,而调用派生或基类方法所需的时间为3个周期。
编译器的优化使密封和未密封的分类之间的性能差异变得如此之低,以至于我们谈论的是处理器界,因此与大多数应用程序无关。