153

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 :)

4

12 に答える 12

153

答えはノーでした。封印されたクラスは、封印されていないクラスよりも優れたパフォーマンスを発揮しません。

2021: 答えはイエスです。クラスを封印することにはパフォーマンス上の利点があります。

クラスを封印しても常にパフォーマンスが向上するとは限りませんが、dotnet チームはすべての内部クラスを封印するルールを採用して、オプティマイザーに最高のチャンスを与えています。

詳細については、https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-6/#peanut-butterをご覧ください。

以下の古い回答。

問題はcallvs callvirtIL オペコードに帰着します。Callより高速でcallvirtcallvirt主にオブジェクトがサブクラス化されているかどうかわからない場合に使用されます。そのため、クラスを封印すると、すべてのオペコードが からcalvirtsに変更されcalls、より高速になると人々は想定しています。

残念ながらcallvirt、null 参照のチェックなど、それを便利にする他のことも行います。これは、クラスが封印されていても参照が null のままである可​​能性があるため、 acallvirtが必要であることを意味します。これを回避することはできますが (クラスを封印する必要はありません)、少し無意味になります。

call構造体はサブクラス化できず、null になることはないため、構造体を使用します。

詳細については、この質問を参照してください。

呼び出しと callvirt

于 2008-12-24T02:09:26.440 に答える
63

JITter は、シールされたクラスのメソッドに非仮想呼び出しを使用することがあります。これは、それらをさらに拡張する方法がないためです。

呼び出しタイプ、仮想/非仮想に関する複雑なルールがあり、私はそれらすべてを知っているわけではないので、実際にそれらを概説することはできませんが、シールされたクラスと仮想メソッドをグーグルで検索すると、トピックに関する記事が見つかるかもしれません.

このレベルの最適化から得られるあらゆる種類のパフォーマンス上の利点は、最後の手段と見なす必要があることに注意してください。コード レベルで最適化する前に、常にアルゴリズム レベルで最適化してください。

これについて言及している 1 つのリンクを次に示します

于 2008-08-05T12:32:40.510 に答える
32

更新: .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) は、次のような同一の 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

私にとって、これは、封印されたクラスと封印されていないクラスでメソッドを呼び出す間にパフォーマンスの向上がないことの確固たる証拠を提供します...私は今幸せだと思います:-)

于 2012-02-20T20:45:28.267 に答える
24

私が知っているように、パフォーマンスが向上するという保証はありません。ただし、封印された方法を使用すると、特定の条件下でパフォーマンスの低下を軽減できる可能性があります。(sealed クラスはすべてのメソッドを封印します。)

ただし、コンパイラの実装と実行環境次第です。


詳細

最新の 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 チップを含む) は、投機的実行と呼ばれる手法を使用して、その状況でもパイプラインを利用します。実行パスの 1 つをプリフェッチするだけです。ただ、この技の命中率はあまり高くありません。また、投機の失敗によってパイプラインが停止し、パフォーマンスが大幅に低下します。(これは完全に CPU の実装によるものです。一部のモバイル CPU は、エネルギーを節約するためにこの種の最適化を行わないことが知られています)

基本的に、C# は静的にコンパイルされる言語です。しかしいつもではない。正確な条件はわかりませんが、これは完全にコンパイラの実装次第です。一部のコンパイラは、メソッドが としてマークされている場合、メソッドのオーバーライドを防止することで動的ディスパッチの可能性を排除できますsealed。愚かなコンパイラはそうではないかもしれません。これが のパフォーマンス上の利点ですsealed


この回答 ( Why is it fast to process a sorted array than an unsorted array? ) は、分岐予測をよりよく説明しています。

于 2011-02-03T15:22:32.063 に答える
5

<off-topic-rant>

私は封印されたクラスを嫌います。パフォーマンス上の利点が驚くべきものであるとしても(私は疑いますが)、継承による再利用を防ぐことにより、オブジェクト指向モデルを破壊します。たとえば、Threadクラスは封印されています。スレッドをできるだけ効率的にしたいと思うかもしれませんが、スレッドをサブクラス化できることが大きなメリットになるシナリオも想像できます。クラスの作成者は、 「パフォーマンス」の理由でクラスを封印する必要がある場合は、少なくとも、忘れた機能が必要な場所でラップして置き換える必要がないように、インターフェイスを提供してください。

例:Threadは封印されており、IThreadインターフェースがないため、SafeThreadはThreadクラスをラップする必要がありました。SafeThreadは、スレッドで未処理の例外を自動的にトラップします。これは、Threadクラスから完全に欠落しているものです。[いいえ、未処理の例外イベントはセカンダリスレッドで未処理の例外を取得しません]。

</ off-topic-rant>

于 2008-10-14T20:04:19.703 に答える
4

クラスをマークしsealedても、パフォーマンスに影響はありません。

オペコードの代わりにオペコードを発行cscしなければならない場合があります。ただし、そのようなケースはまれのようです。callvirtcall

そして、クラスに(まだ)サブクラスがないことがわかっている場合callvirt、JITは と同じ非仮想関数呼び出しを発行できるはずだと私には思えます。callメソッドの実装が 1 つしか存在しない場合、そのアドレスを vtable からロードしても意味がありません。その 1 つの実装を直接呼び出すだけです。さらに言えば、JIT は関数をインライン化することもできます。

サブクラスが後でロードされた場合、JIT はそのマシン コードを破棄してコードを再度コンパイルし、実際の仮想呼び出しを発行する必要があるため、JIT 側では少しギャンブルです。私の推測では、これは実際にはあまり起こりません。

(そして、そうです、VM 設計者は実際に、これらのわずかなパフォーマンスの向上を積極的に追求しています。)

于 2009-11-20T09:53:29.783 に答える
3

私は「封印された」クラスを通常のケースと考えており、「封印された」キーワードを省略する理由は常にあります。

私にとって最も重要な理由は次のとおりです。

a) コンパイル時のチェックの改善 (実装されていないインターフェースへのキャストは、実行時だけでなくコンパイル時にも検出されます)

そして、一番の理由:

b)そのように私のクラスを悪用することはできません

マイクロソフトが「封印されていない」ではなく、「封印されている」を標準にしたことを願っています。

于 2010-08-03T14:27:15.043 に答える
3

封印されたクラスにより、パフォーマンスが向上するはずです。シール クラスは派生できないため、任意の仮想メンバーを非仮想メンバーに変えることができます。

もちろん、私たちは本当に小さな利益について話している. プロファイリングで問題が明らかにならない限り、パフォーマンスを向上させるためだけにクラスをシール済みとしてマークすることはありません。

于 2008-08-05T12:37:07.067 に答える
2

シールされたクラスは、少なくともわずかに高速になりますが、場合によっては非常に高速になることがあります... JITオプティマイザーが、そうでなければ仮想呼び出しであった呼び出しをインライン化できる場合。したがって、インライン化できるほど小さいメソッドが頻繁に呼び出される場合は、クラスを封印することを検討してください。

しかし、クラスを封印する一番の理由は、「私はこれを継承するように設計したわけではないので、継承するように設計されていると仮定してあなたを怒らせるつもりはありません。実装から派生させたので、実装に閉じ込められて自分を燃やします。」

ここにいる何人かは、何かから派生する機会が欲しいので、シールされたクラスが嫌いだと言っていることを知っています...しかし、それは多くの場合、最も保守可能な選択ではありません...クラスを派生に公開すると、すべてを公開しないよりもはるかにロックされるためですそれ。これは、「プライベート メンバーを持つクラスは嫌いです...アクセス権がないため、クラスに自分のやりたいことをさせることができないことがよくあります」と言っているのと似ています。カプセル化は重要です... シーリングはカプセル化の一形態です。

于 2011-10-01T10:37:28.847 に答える
-11

このコードを実行すると、sealed クラスが 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";
    }
}

出力: Sealed クラス: 00:00:00.1897568 NonSealed クラス: 00:00:00.3826678

于 2009-11-26T17:50:14.860 に答える