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

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

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

帮我学习:)

===============>>#1 票数:138

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

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

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

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

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

通话和通话

===============>>#2 票数:56 已采纳

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

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

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

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

===============>>#3 票数:25

更新:从.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方法直接内联到调用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

对我来说,这提供了有力的证据,表明在密封类和非密封类上的调用方法之间不会有任何性能改进……我想我现在很高兴:-)

===============>>#4 票数:23

据我所知,不能保证性能的提高。 但是在某些特定条件下使用密封方法可以降低性能损失 (密封类使所有方法都被密封。)

但这取决于编译器的实现和执行环境。


细节

许多现代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性能的好处。


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

===============>>#5 票数:4

将类标记为已sealed不应对性能产生影响。

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

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

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

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

===============>>#6 票数:4

<题外话>

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

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

</ off-topic-rant>

===============>>#7 票数:3

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

当然,我们说的是很小的收获。 除非剖析表明存在问题,否则我不会将一个类标记为仅密封以提高性能。

===============>>#8 票数:3

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

对我来说,最重要的原因是:

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

并且,首要原因:

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

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

===============>>#9 票数:2

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

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

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

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

===============>>#10 票数:2

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

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

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

===============>>#11 票数:-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

  ask by Vaibhav translate from so

未解决问题?本站智能推荐: