密封课程真的可以带来绩效收益吗?


140

我遇到了很多优化技巧,这些技巧说您应该将类​​标记为已密封,以获得更多的性能优势。

我进行了一些测试以检查性能差异,但没有发现。难道我做错了什么?我是否错过了密封类会带来更好结果的情况?

有没有人进行测试并发现差异?

帮我学习:)


8
我不认为密封类旨在提高性能。他们这样做的事实可能是偶然的。除此之外,在将应用程序重构为使用密封类后,对其进行概要分析,并确定是否值得付出努力。从长远来看,限制可扩展性以进行不必要的微优化将使您付出代价。当然,如果您进行了概要分析,并且可以达到性能基准(而不是为了性能而追求性能),那么如果值得花钱,则可以作为一个团队做出决定。如果您出于非善意的原因参加过密封课程,请保持em :)
Merlyn Morgan-Graham 2010年

1
您尝试过反射吗?我在某处读到,使用密封的类通过反射实例化的速度更快
onof

据我所知没有。密封的原因是不同的-阻止可扩展性,这在很多情况下可能是有用的/需要的。性能优化不是这里的目标。
TomTom'9

...但是,如果考虑一下编译器:如果您的类是密封的,则您知道在编译时在类上调用的方法的地址。如果您的类不是密封的,则必须在运行时解析该方法,因为您可能需要调用重写。当然可以忽略不计,但是我可以看到会有一些区别。
Dan Puzey

是的,但是正如OP所指出的那样,这并不能转化为真正的好处。体系结构上的差异/可能更相关。
TomTom'9

Answers:


60

JITter有时会在密封类中使用对方法的非虚拟调用,因为无法进一步扩展它们。

关于调用类型,虚拟/非虚拟类型有复杂的规则,我不了解它们的全部,因此我无法真正为您概述它们,但是如果您搜索密封类和虚拟方法,则可能会找到有关该主题的文章。

请注意,您将从此优化级别获得的任何类型的性能收益都应视为最后解决方案,始终在算法级别进行优化,然后再在代码级别进行优化。

这是一个提到此的链接:在密封关键字上乱逛


2
“杂乱无章”的链接很有趣,因为它听起来像技术上的优势,但实际上是胡说八道。阅读文章上的评论以获取更多信息。摘要:给出的3个原因是版本控制,性能和安全性/可预测性-[请参阅下一条评论]
Steven A. Lowe

1
[续]版本控制仅适用于没有子类的情况,但是,将此参数扩展到每个类时,突然间您没有继承性并且猜测什么,该语言不再是面向对象的(而是仅基于对象的)![请参阅下一篇]
史蒂文·A·劳

3
[续]性能示例是一个笑话:优化虚拟方法调用;为什么密封的类首先会具有虚拟方法,因为它不能被子类化?最后,“安全性/可预测性”论点显然是错误的:“您不能使用它,因此它是安全/可预测的”。大声笑!
史蒂文·A·劳

16
@Steven A. Lowe-我认为Jeffrey Richter试图以一种round回的方式说的是,如果您不打开类,则需要考虑派生类如何/将使用它,并且如果您没有时间或倾向正确执行此操作,然后将其密封,因为将来不太可能导致破坏他人代码的更改。那根本不是废话,这是很好的常识。
Greg Beech,2009年

6
密封的类可能具有虚方法,因为它可能派生自声明它的类。之后,当您声明密封后代类的变量并调用该方法时,编译器可能会直接调用已知的实现,因为它知道这与已知的方法没有什么不同,该类的编译时vtable。至于密封/未密封,那是另一番讨论,我同意默认情况下使类密封的原因。
Lasse V. Karlsen,

143

答案是否定的,密封类的性能不会比非密封类好。

问题归结于callvs callvirtIL操作码。Call比快callvirt,并且callvirt主要在您不知道对象是否已被子类化时使用。因此人们认为,如果您密封一个班级,那么所有操作码都会从更改为calvirtscalls并且操作速度会更快。

不幸的是callvirt,还有其他使它有用的事情,例如检查空引用。这意味着即使一个类是密封的,引用也可能仍然为空,因此callvirt需要a。您可以解决这个问题(而无需密封类),但是它变得毫无意义。

使用结构call是因为它们不能被子类化,并且永远不能为null。

有关更多信息,请参见此问题:

通话和通话


5
在AFAIK中,使用的情况call是:在情况中new T().Method(),用于struct方法,用于方法的非虚拟调用virtual(例如base.Virtual())或用于static方法。其他任何地方都使用callvirt
凌晨

1
嗯...我意识到这很老了,但这不是很正确...密封类的最大胜利是当JIT Optimizer可以内联调用时...在这种情况下,密封类可能是一个巨大的胜利。
布莱恩·肯尼迪

5
为什么这个答案是错误的。来自Mono更改日志:“针对密封类和方法的虚拟化优化,将IronPython 2.0 pystone的性能提高了4%。其他程序可以期望得到类似的改进[Rodrigo]。”。密封的类可以提高性能,但是一如既往,它取决于情况。
Smilediver

1
@Smilediver它可以提高性能,但前提是您的JIT不好(尽管现在不知道.NET JIT的表现如何-过去在这方面非常糟糕)。例如,热点将内联虚拟调用,并在必要时稍后进行优化-因此,仅当您实际继承该类(甚至不一定)时,才需要支付额外的开销。
Voo 2012年

1
-1 JIT不一定必须为相同的IL操作码生成相同的机器代码。空检查和虚拟调用是callvirt的正交步骤和独立步骤。对于密封类型,JIT编译器仍然可以优化部分callvirt。当JIT编译器可以保证引用不会为null时,情况也是如此。
2014年

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在Release构建配置中)发出相同的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

我很困惑:为什么callvirt这些方法不是虚拟的,为什么会出现呢?
怪胎

@freakish我不记得我在哪里看到的,但是我读到CLR用于callvirt密封方法,因为它仍然必须在调用方法调用之前对对象进行空检查,并且一旦考虑到这一点,您也可以使用callvirt。要删除callvirt和直接跳转,他们要么需要((string)null).methodCall()像C ++一样修改C#以允许使用,要么需要静态证明对象不是null(他们可以这样做,但没有打扰到)
Orion Edwards

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(包括最近的Intel x86芯片)使用一种称为推测执行的技术来利用流水线,即使在这种情况下也是如此。只需预取执行路径之一即可。但是该技术的命中率不是很高。投机失败会导致管道停顿,这也会造成巨大的性能损失。(这完全是由CPU实现的。某些移动CPU被称为没有这种优化以节省能源)

基本上,C#是一种静态编译的语言。但不总是。我不知道确切的条件,这完全取决于编译器的实现。如果方法标记为,则某些编译器可以通过防止方法覆盖来消除动态调度的可能性sealed。愚蠢的编译器可能不会。这是的性能优势sealed


这个答案(为什么处理排序数组比未排序数组更快?)描述了分支预测好得多。


1
奔腾类CPU直接预取间接调度。出于这个原因,有时函数指针重定向比if(unguessable)更快。
约书亚

2
非虚函数或密封函数的优点之一是可以在更多情况下内联它们。
CodesInChaos

4

<题外话>

讨厌密封班。即使性能优势惊人(我对此表示怀疑),它们也会通过防止通过继承进行重用而破坏面向对象的模型。例如,Thread类是密封的。虽然我可以看到人们可能希望线程尽可能地高效,但我也可以想象能够将Thread子类化的巨大好处。类作者,如果您出于“性能”的原因必须密封类,至少提供一个接口,这样我们就不必在所有我们需要您忘记的功能的地方进行包装和替换。

示例:SafeThread必须包装Thread类,因为Thread是密封的,并且没有IThread接口。SafeThread会自动捕获线程上未处理的异常,这些异常在Thread类中完全丢失。[并且不,未处理的异常事件不会在辅助线程中拾取未处理的异常]。

</ off-topic-rant>


35
出于性能方面的考虑,我不会上课。我出于设计原因将它们密封。为继承而设计是很难的,而且大多数时间都会浪费精力。我完全同意提供接口- 对于解封类而言,这是一个非常出色的解决方案。
乔恩·斯基特

6
封装通常是比继承更好的解决方案。以特定的线程示例为例,捕获线程异常违反了Liskov替代原则,因为您已经更改了Thread类的已记录行为,因此即使可以从中继承该类,也不能说可以在任何地方使用SafeThread都是不合理的。您可以使用线程。在这种情况下,最好将Thread封装到具有不同记录行为的另一个类中,您可以执行此操作。有时候,事情为了自己的利益而被密封。
格雷格·比奇

1
@ [Greg Beech]:观点,不是事实-能够从Thread继承来修复其设计中令人发指的疏忽不是一件坏事;-)而且我认为您夸大了LSP-可证明的属性q(x)在这种情况是“未处理的异常会破坏程序”,这不是“理想的属性” :-)
Steven A. Lowe

1
不,但是我分享了一些糟糕的代码,在这些代码中,我使其他开发人员可以通过不密封或允许极端用例来滥用我的东西。如今,我的大多数代码都是断言和其他与合同有关的东西。我很开放这一事实,因为我这样做只会让***感到痛苦。
图灵完成了2010年

2
由于我们在这里做题外话,就像您讨厌密封类一样,所以我讨厌吞咽的异常。没有什么比事情变得更糟而程序继续执行更糟糕的了。JavaScript是我的最爱。您更改了一些代码,突然单击一个按钮绝对没有任何作用。大!ASP.NET和UpdatePanel是另一个。严重的是,如果我的按钮处理程序抛出了一个大问题,并且需要崩溃,那么我知道有些事情需要修复!什么都不做的按钮比弹出崩溃屏幕的按钮无用!
罗曼·斯塔科夫

4

标记一个类sealed应该不会对性能产生影响。

在某些情况下,csc可能必须发出callvirt操作码而不是call操作码。但是,这些情况似乎很少见。

在我看来,如果JIT 知道该类还没有任何子类(尚未)callvirt,它应该能够发出与该请求相同的非虚拟函数调用call。如果仅存在该方法的一个实现,则从vtable加载其地址毫无意义,只需直接调用一个实现即可。为此,JIT甚至可以内联功能。

这是一个有点对JIT的部分赌博的,因为如果一个子类后加载,JIT将不得不扔掉机器代码,并重新编译代码,散发出真实的虚拟呼叫。我的猜测是在实践中这种情况很少发生。

(是的,VM设计人员确实确实在积极追求这些微小的性能优势。)


3

密封的类可提高性能。由于无法派生密封类,因此任何虚拟成员都可以变成非虚拟成员。

当然,我们说的是很小的收获。除非剖析显示它是一个问题,否则我不会将一个类标记为封闭的只是为了提高性能。


他们应该,但似乎他们没有。如果CLR具有非空引用类型的概念,那么密封类确实会更好,因为编译器可以发出call而不是callvirt...我也很喜欢非空引用类型,原因很多。叹息:-(
Orion Edwards

3

我认为“密封”类是正常情况,我总是有理由省略“密封”关键字。

对我而言,最重要的原因是:

a)更好的编译时检查(将在编译时(不仅在运行时)检测到未实现的接口的广播)

并且,首要原因:

b)这样不可能滥用我的课程

我希望微软能够使“密封”成为标准,而不是“未密封”。


我相信“遗漏”(遗漏)应该是“遗漏”(产生)吗?
user2864740 '17

2

@Vaibhav,您执行了哪种测试来衡量性能?

我猜想人们将不得不使用Rotor并深入研究CLI并了解密封类如何提高性能。

SSCLI(转子)
SSCLI:共享源通用语言基础结构

通用语言基础结构(CLI)是ECMA标准,描述了.NET Framework的核心。共享源CLI(SSCLI),也称为Rotor,是源代码的压缩存档,用于ECMA CLI和ECMA C#语言规范的有效实现,这些技术是Microsoft .NET体系结构的核心。


该测试包括创建一个类层次结构,其中包含一些正在做虚拟工作(主要是字符串操作)的方法。其中一些方法是虚拟的。他们在这里和那里互相打电话。然后,将这些方法分别调用100、10000和100000次,并测量经过的时间。然后在将类标记为已密封之后运行它们。并再次测量。他们没什么区别。
Vaibhav,

2

如果JIT Optimizer可以内联调用,否则密封类将至少快一点,但是有时可以更快一些。。。因此,在经常调用的方法足够小以内联的地方,绝对可以考虑密封该类。

但是,密封类的最佳理由是说“我没有将其设计为从继承而来,因此,我不会通过假定它是这样设计而让您感到厌倦的,因此,我不会通过锁定到实现中来烧死自己,因为我让您从中获得好处。”

我知道这里有些人说他们讨厌密封类,因为他们希望有机会从任何东西中派生……但这通常不是最可维护的选择……因为将类暴露给派生会使您锁定很多,而不是不暴露所有对象。那。这类似于说“我讨厌具有私有成员的类……我经常无法使类做我想要的事情,因为我没有访问权限。” 封装很重要...密封是封装的一种形式。


C#编译器仍将callvirt对密封类的实例方法使用(虚拟方法调用),因为它仍必须对它们进行空对象检查。关于内联,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(移动指令)后,调用密封方法,在dword ptr [ecx],ecx(cmp指令)之间执行比较,然后在派生/基类直接执行该方法的同时调用该方法。 。

根据Torbjorn 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
这有两个问题。首先,您无需在第一次测试和第二次测试之间重置秒表。其次,您调用该方法的方式意味着所有操作码都将被调用而不是callvirt,因此类型无关紧要。
卡梅隆·麦克法兰

1
RuslanG,运行第一个测试后,您忘记了调用watch.Reset()。o_O:]

是的,上面的代码有错误。再说一次,谁能自吹自to,从不引入代码错误? 但是,这个答案比其他所有答案要重要的是:它试图衡量所讨论的效果。而且此答案还为所有您检查和[质量降低]的投票共享代码(我想知道为什么需要大量降低质量?)。与其他答案相反。我认为这值得尊重...另外请注意,该用户是新手,因此也不是非常欢迎的方法。一些简单的修复,此代码对我们所有人都有用。如果您敢,请修复+分享固定版本
Roland Pihlakas

我不同意@RolandPihlakas。如您所说,“谁可以自吹自to,永远不会在代码中引入错误”。答案->不,没有。但这不是重点。关键是,这是一个错误的信息,可能会误导另一个新程序员。许多人很容易会错过秒表未重置的情况。他们可以相信基准信息是真实的。那不是更有害吗?快速寻找答案的人甚至可能没有阅读这些评论,而是他/她会看到答案,并且他/她可能会相信。
Emran Hussain
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.