.net 密封类真的能提供性能优势吗?
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/2134/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Do sealed classes really offer performance Benefits?
提问by Vaibhav
I have come across a lot of optimization tips which say that you should mark your classes as sealed to get extra performance benefits.
我遇到了很多优化技巧,其中说您应该将类标记为密封的以获得额外的性能优势。
I ran some tests to check the performance differential and found none. Am I doing something wrong? Am I missing the case where sealed classes will give better results?
我运行了一些测试来检查性能差异,但没有发现。难道我做错了什么?我是否错过了密封类会产生更好结果的情况?
Has anyone run tests and seen a difference?
有没有人运行测试并看到不同之处?
Help me learn :)
帮我学习:)
采纳答案by Lasse V. Karlsen
The JITter will sometimes use non-virtual calls to methods in sealed classes since there is no way they can be extended further.
JITter 有时会使用对密封类中方法的非虚拟调用,因为它们无法进一步扩展。
There are complex rules regarding calling type, virtual/nonvirtual, and I don't know them all so I can't really outline them for you, but if you google for sealed classes and virtual methods you might find some articles on the topic.
关于调用类型、虚拟/非虚拟,有一些复杂的规则,我并不全部都知道,所以我无法为您真正概述它们,但是如果您在 google 上搜索密封类和虚拟方法,您可能会找到一些关于该主题的文章。
Note that any kind of performance benefit you would obtain from this level of optimization should be regarded as last-resort, always optimize on the algorithmic level before you optimize on the code-level.
请注意,您从这种优化级别获得的任何类型的性能优势都应被视为最后的手段,始终在算法级别进行优化,然后再在代码级别进行优化。
Here's one link mentioning this: Rambling on the sealed keyword
这是一个提到这一点的链接:Rambling on theealed 关键字
回答by Cameron MacFarland
The answer is no, sealed classes do not perform better than non-sealed.
答案是否定的,密封类的性能并不比非密封类好。
The issue comes down to the callvs callvirtIL op codes. Callis faster than callvirt, and callvirtis mainly used when you don't know if the object has been subclassed. So people assume that if you seal a class all the op codes will change from calvirtsto callsand will be faster.
问题归结为callvs callvirtIL 操作码。Call比 快callvirt,callvirt主要用于不知道对象是否已被子类化的情况。所以人们假设如果你密封一个类,所有的操作码都会从calvirts变为calls并且会更快。
Unfortunately callvirtdoes other things that make it useful too, like checking for null references. This means that even if a class is sealed, the reference might still be null and thus a callvirtis needed. You can get around this (without needing to seal the class), but it becomes a bit pointless.
不幸的是callvirt,还有其他使它有用的事情,比如检查空引用。这意味着即使一个类是密封的,引用可能仍然为空,因此callvirt需要 a。你可以解决这个问题(不需要密封类),但它变得有点毫无意义。
Structs use callbecause they cannot be subclassed and are never null.
使用结构call是因为它们不能被子类化并且永远不会为空。
See this question for more information:
有关更多信息,请参阅此问题:
回答by Orion Edwards
Update: As of .NET Core 2.0 and .NET Desktop 4.7.1, the CLR now supports devirtualization. It can take methods in sealed classes and replace virtual calls with direct calls - and it can also do this for non-sealed classes if it can figure out it's safe to do so.
更新:从 .NET Core 2.0 和 .NET Desktop 4.7.1 开始,CLR 现在支持去虚拟化。它可以采用密封类中的方法并用直接调用替换虚拟调用 - 如果它可以确定这样做是安全的,它也可以对非密封类执行此操作。
In such a case (a sealed class that the CLR couldn't otherwise detect as safe to devirtualise), a sealed class should actually offer some kind of performance benefit.
在这种情况下(CLR 无法通过其他方式检测到可以安全地进行去虚拟化的密封类),密封类实际上应该提供某种性能优势。
That said, I wouldn't think it'd be worth worrying about unlessyou had already profiled the code and determined that you were in a particularly hot path being called millions of times, or something like that:
也就是说,我认为这不值得担心,除非您已经对代码进行了概要分析并确定您处于被调用数百万次的特别热门的路径中,或者类似的事情:
Original Answer:
原答案:
I made the following test program, and then decompiled it using Reflector to see what MSIL code was emitted.
我做了下面的测试程序,然后用 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");
}
In all cases, the C# compiler (Visual studio 2010 in Release build configuration) emits identical MSIL, which is as follows:
在所有情况下,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
The oft-quoted reason that people say sealed provides performance benefits is that the compiler knows the class isn't overriden, and thus can use callinstead of callvirtas it doesn't have to check for virtuals, etc. As proven above, this is not true.
人们经常引用的原因是 Sealed 提供了性能优势,因为编译器知道该类没有被覆盖,因此可以使用call代替,callvirt因为它不必检查虚拟等。如上所述,这不是真的。
My next thought was that even though the MSIL is identical, perhaps the JIT compiler treats sealed classes differently?
我的下一个想法是,即使 MSIL 是相同的,JIT 编译器是否可能会以不同的方式对待密封类?
I ran a release build under the visual studio debugger and viewed the decompiled x86 output. In both cases, the x86 code was identical, with the exception of class names and function memory addresses (which of course must be different). Here it is
我在 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
I then thought perhaps running under the debugger causes it to perform less aggressive optimization?
然后我想也许在调试器下运行会导致它执行不那么积极的优化?
I then ran a standalone release build executable outside of any debugging environments, and used WinDBG + SOS to break in after the program had completed, and view the dissasembly of the JIT compiled x86 code.
然后我在任何调试环境之外运行了一个独立的发布构建可执行文件,并在程序完成后使用 WinDBG + SOS 闯入,并查看 JIT 编译的 x86 代码的反汇编。
As you can see from the code below, when running outside the debugger the JIT compiler is more aggressive, and it has inlined the WriteItmethod straight into the caller.
The crucial thing however is that it was identical when calling a sealed vs non-sealed class. There is no difference whatsoever between a sealed or nonsealed class.
正如您从下面的代码中看到的那样,在调试器之外运行时,JIT 编译器更加激进,它将WriteIt方法直接内联到调用者中。然而,关键是在调用密封类与非密封类时它是相同的。密封类和非密封类之间没有任何区别。
Here it is when calling a normal class:
这是在调用普通类时:
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
Vs a sealed class:
与密封类:
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
To me, this provides solid proof that there cannotbe any performance improvement between calling methods on sealed vs non-sealed classes... I think I'm happy now :-)
对我来说,这提供了确凿的证据,证明在密封类和非密封类上调用方法之间不会有任何性能改进......我想我现在很高兴:-)
回答by Eonil
As I know, there is no guarantee of performance benefit. But there is a chance to decrease performance penalty under some specific conditionwith 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.
许多现代 CPU 使用长管道结构来提高性能。因为 CPU 比内存快得难以置信,所以 CPU 必须从内存中预取代码以加速流水线。如果代码没有在适当的时候准备好,管道将处于空闲状态。
There is a big obstacle called dynamic dispatchwhich 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 hazardcauses pipeline idle. And performance penalty by idle is huge in regular.
在这种情况下,CPU 无法预取要执行的下一个代码,因为在解决条件之前下一个代码位置是未知的。所以这使得危险导致管道空闲。空闲的性能损失在常规情况下是巨大的。
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 executionto 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)
一些 CPU(包括最近的 Intel 的 x86 芯片)使用称为推测执行的技术,即使在这种情况下也能利用管道。只需预取执行路径之一。但是这个技能的命中率并没有那么高。推测失败会导致管道停顿,这也会造成巨大的性能损失。(这完全是由CPU实现的。一些手机CPU号称没有这种优化来节能)
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.
基本上,C# 是一种静态编译语言。但不总是。我不知道确切的条件,这完全取决于编译器的实现。如果方法被标记为 ,一些编译器可以通过防止方法覆盖来消除动态调度的可能性sealed。愚蠢的编译器可能不会。这是sealed.
This answer (Why is it faster to process a sorted array than an unsorted array?) is describing the branch prediction a lot better.
这个答案(为什么处理排序数组比处理未排序数组更快?)更好地描述了分支预测。
回答by Jason Orendorff
Marking a class sealedshould have no performance impact.
标记一个类sealed应该没有性能影响。
There are cases where cscmight have to emit a callvirtopcode instead of a callopcode. However, it seems those cases are rare.
在某些情况下,csc可能必须发出callvirt操作码而不是call操作码。然而,这些情况似乎很少见。
And it seems to me that the JIT should be able to emit the same non-virtual function call for callvirtthat 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.
在我看来,如果 JIT知道该类没有任何子类(还)callvirt,它应该能够发出与 for 相同的非虚拟函数调用call。如果该方法只存在一种实现,则从 vtable 加载其地址没有意义——只需直接调用一种实现即可。就此而言,JIT 甚至可以内联该函数。
It's a bit of a gamble on the JIT's part, because if a subclass islater 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.
这是一个有点对JIT的部分赌博的,因为如果一个子类是后加载,JIT将不得不扔掉机器代码,并重新编译代码,散发出真实的虚拟呼叫。我的猜测是这在实践中并不经常发生。
(And yes, VM designers really do aggressively pursue these tiny performance wins.)
(是的,VM 设计人员确实积极追求这些微小的性能优势。)
回答by Steven A. Lowe
<off-topic-rant>
<题外话>
I loathesealed classes. Even if the performance benefits are astounding (which I doubt), they destroythe 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 mustseal your classes for "performance" reasons, please provide an interfaceat the very least so we don't have to wrap-and-replace everywhere that we need a feature you forgot.
我讨厌密封的课程。即使性能优势令人震惊(我对此表示怀疑),它们也会通过继承阻止重用来破坏面向对象的模型。例如,Thread 类是密封的。虽然我可以看到人们可能希望线程尽可能高效,但我也可以想象能够对 Thread 进行子类化会带来很大好处的场景。类作者,如果您出于“性能”原因必须密封您的类,请至少提供一个接口,这样我们就不必在需要您忘记的功能的任何地方进行包装和替换。
Example: SafeThreadhad 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 notpick up unhandled exceptions in secondary threads].
示例:SafeThread不得不将 Thread 类包装起来,因为 Thread 是密封的,没有 IThread 接口;SafeThread 自动捕获线程上未处理的异常,这是 Thread 类中完全缺失的。[不,未处理的异常事件不会在辅助线程中拾取未处理的异常]。
</off-topic-rant>
</off-topic-rant>
回答by Turing Complete
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)
a) 更好的编译时检查(转换到未实现的接口将在编译时被检测到,而不仅仅是在运行时)
and, top reason:
而且,首要原因:
b) Abuse of my classes is not possible that way
b) 以这种方式滥用我的课程是不可能的
I wish Microsoft would have made "sealed" the standard, not "unsealed".
我希望微软将“密封”作为标准,而不是“未密封”。
回答by Karl Seguin
Sealed classes shouldprovide 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.
当然,我们说的是非常小的收益。我不会仅仅为了提高性能而将一个类标记为密封的,除非分析表明它是一个问题。
回答by hitec
@Vaibhav, what kind of tests did you execute to measure performance?
@Vaibhav,您执行了哪些测试来衡量性能?
I guess one would have to use Rotorand to drill into CLI and understand how a sealed class would improve performance.
我想人们必须使用Rotor并深入了解 CLI 并了解密封类将如何提高性能。
SSCLI (Rotor)
SSCLI: Shared Source Common Language InfrastructureThe 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.
SSCLI (Rotor)
SSCLI:共享源公共语言基础设施公共语言基础结构 (CLI) 是 ECMA 标准,描述了 .NET Framework 的核心。共享源 CLI (SSCLI),也称为 Rotor,是 ECMA CLI 和 ECMA C# 语言规范的工作实现的源代码的压缩存档,这些技术是 Microsoft .NET 架构的核心技术。
回答by Brian Kennedy
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.
密封类至少会快一点,但有时可能会更快……如果 JIT 优化器可以内联调用,否则本来是虚拟调用。因此,如果经常调用的方法小到可以内联,那么一定要考虑密封类。
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.
我知道这里有些人说他们讨厌密封类,因为他们希望有机会从任何东西中派生……但这通常不是最易于维护的选择……因为将类暴露于派生中比不公开所有更能锁定你那。它类似于说“我讨厌有私有成员的类......我经常无法让类做我想做的事情,因为我没有访问权限。” 封装很重要……密封是封装的一种形式。

