シールドクラスは本当にパフォーマンス上のメリットを提供しますか?


140

パフォーマンスをさらに向上させるには、クラスをシール済みとしてマークする必要があるという、多くの最適化のヒントを見つけました。

パフォーマンスの違いを確認するためにいくつかのテストを実行したところ、何も見つかりませんでした。私は何か間違ったことをしていますか?密封されたクラスがより良い結果をもたらすケースを見逃していますか?

誰かがテストを実行して違いを見ましたか?

学ぶのを手伝ってください:)


9
封印されたクラスがパフォーマンスの向上を目的としたものではないと思います。彼らがそうするという事実は偶発的かもしれません。さらに、シールされたクラスを使用するようにリファクタリングした後、アプリのプロファイルを作成し、努力する価値があるかどうかを判断します。拡張機能を制限して不要なマイクロ最適化を行うと、長期的にはコストがかかります。もちろん、プロファイルを作成し、パフォーマンスのベンチマーク(パフォーマンスのためのパフォーマンスではなく)を達成できる場合は、費やす価値があるかどうかをチームとして決定できます。あなたは非PERFの理由のためのクラスを密封されている場合は、:) EMを保つ
メルリンモルガン・グラハム

1
リフレクションで試しましたか?私は反射によってインスタンス化することは密封されたクラスを使用して高速であることをどこかで読ん
ONOF

私の知る限りではありません。封印は別の理由で存在します-多くの場合に役立つ/必要になる拡張性をブロックするためです。ここでは、パフォーマンスの最適化は目標ではありませんでした。
TomTom

...しかし、コンパイラについて考える場合:クラスがシールされている場合、コンパイル時にクラスで呼び出すメソッドのアドレスがわかります。クラスがシールされていない場合は、オーバーライドを呼び出す必要がある可能性があるため、実行時にメソッドを解決する必要があります。それは確かに無視できるでしょうが、私はいくつかの違いがあることがわかりました。
Dan Puzey

はい、しかしOPが指摘したように、そのようなことは実際の利益にはなりません。アーキテクチャの違いは、はるかに関連性があります。
TomTom

回答:


60

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

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

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

:ここでは、この言及一つのリンクだ密封されたキーワードのランブリングは、


2
「とりとめのない」リンクは、技術的に優れているように聞こえますが、実際にはナンセンスです。詳細については、記事のコメントをご覧ください。要約:指定された3つの理由は、バージョニング、パフォーマンス、セキュリティ/予測可能性です-[次のコメントを参照]
Steven A. Lowe

1
[続き]バージョン管理はサブクラスがない場合にのみ適用されますが、この引数をすべてのクラスに拡張すると、突然継承がなくなり、何が起こったのか、言語はオブジェクト指向ではなくなります(ただし、オブジェクトベースのみ)。[次を参照]
スティーブンA.ロウ

3
[続き]パフォーマンスの例は冗談です:仮想メソッド呼び出しの最適化。なぜ封印されたクラスはサブクラス化できないので、そもそも仮想メソッドを持つのでしょうか?最後に、セキュリティ/予測可能性の議論は明らかに不確実です:「それを使用できないので、安全/予測可能です」。笑!
スティーブンA.ロウ

16
@Steven A. Lowe-ジェフリー・リヒターがやや回り道で言っていたのは、クラスを封印しないままにしておくと、派生クラスがそれをどのように使用できるか、または使用するか、そして、これを適切に行うための時間または傾向。その後、他の人のコードに重大な変更を引き起こす可能性が低いので、それを封印します。それはまったくナンセンスではありません、それは常識です。
グレッグビーチ、

6
シールされたクラスは、それを宣言するクラスから派生する可能性があるため、仮想メソッドを持つ場合があります。その後、シールされた子孫クラスの変数を宣言し、そのメソッドを呼び出すと、コンパイラーは既知の実装への直接呼び出しを発行する可能性があります。そのクラスのコンパイル時のvtable。封印/封印解除については、それは別の議論であり、デフォルトでクラスを封印にする理由に同意します。
Lasse V. Karlsen、

143

答えはノーです。密封されたクラスは、密封されていないクラスよりもパフォーマンスが良くありません。

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

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

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

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

呼び出しとcallvirt


5
私の知る限り、call使用される状況は次のとおりです。状況new T().Method()structメソッドの場合、メソッドへの非仮想呼び出しvirtual(などbase.Virtual())、またはstaticメソッドの場合。他のどこでも使用しcallvirtます。
ポルジェス

1
ええと...これは古いのですが、これは正しくありません...密封されたクラスの大きな勝利は、JITオプティマイザーが呼び出しをインライン化できるときです...その場合、密封されたクラスは大きな勝利になる可能性があります。
ブライアンケネディ

5
なぜこの答えは間違っています。Mono changelogから:「封印されたクラスとメソッドの仮想化の最適化、IronPython 2.0 pystoneパフォーマンスの4%の向上。他のプログラムでも同様の向上が期待できます[Rodrigo]。」密封されたクラスはパフォーマンスを向上させることができますが、いつものように、それは状況に依存します。
スマイルダイバー、

1
@Smilediverパフォーマンスを向上させることができますが、JITが悪い場合のみです(ただし、.NET JITが最近どれほど優れているかはわかりません。以前はその点でかなり悪かったのです)。たとえば、Hotspotは仮想呼び出しをインライン化し、後で必要に応じて最適化を解除します。したがって、実際にクラスをサブクラス化する場合にのみ追加のオーバーヘッドを支払うことになります(その必要はありません)。
Voo

1
-1 JITは、必ずしも同じILオペコードに対して同じマシンコードを生成する必要はありません。ヌルチェックと仮想呼び出しは直交し、callvirtの個別のステップです。封印された型の場合でも、JITコンパイラはcallvirtの一部を最適化できます。JITコンパイラが参照がnullにならないことを保証できる場合も同じです。
14

29

更新:.NET Core 2.0および.NET Desktop 4.7.1以降、CLRは非仮想化をサポートするようになりました。シールクラスのメソッドを取得し、仮想呼び出しを直接呼び出しに置き換えることができます。また、安全であることがわかっている場合は、非シールクラスに対してもこれを行うことができます。

そのような場合(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

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


もう一方を重複して閉じるのではなく、この回答を2回投稿した理由はありますか?これは私の領域ではありませんが、それらは私には有効な複製のように見えます。
フレキソ

1
インライン化を避け、どの呼び出し操作が使用されるかを確認するために、メソッドがより長い(32バイトのILコードを超える)場合はどうなりますか?インライン化されている場合、呼び出しが表示されないため、効果を判断できません。
ygoe 2015年

私は混乱しています:callvirtこれらのメソッドが最初から仮想化されていないのになぜそこにあるのですか?
気まぐれ

@freakish私はこれをどこで見たのか思い出せませんがcallvirt、メソッド呼び出しを呼び出す前にオブジェクトをnullチェックする必要があるため、シールされたメソッドにCLRが使用されていることを読みました。使用しますcallvirt。を削除してcallvirt直接ジャンプするに((string)null).methodCall()は、C ++のようにC#を変更できるようにするか、オブジェクトがnullではないことを静的に証明する必要があります(これは可能ですが、気にしていません)。
オリオンエドワーズ

1
マシンコードレベルまで掘り下げる努力をすることの小道具ですが、「これはパフォーマンスの向上がないことの確固たる証拠を提供します」のようなステートメントの作成には十分注意してください。あなたが示したことは、ある特定のシナリオでは、ネイティブ出力に違いはないということです。これは1つのデータポイントであり、すべてのシナリオに一般化するとは限りません。手始めに、クラスは仮想メソッドを定義しないため、仮想呼び出しはまったく必要ありません。
Matt Craig、

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


この答え(ソートされていない配列よりもソートされた配列を処理する方が速いのはなぜですか?)は、分岐予測をよりよく説明しています。


1
PentiumクラスのCPUは、間接ディスパッチを直接プリフェッチします。このため、関数ポインタのリダイレクトがif(ungesssable)よりも高速な場合があります。
ジョシュア

2
非仮想関数またはシール関数の1つの利点は、より多くの状況でインライン化できることです。
CodesInChaos、2011

4

<off-topic-rant>

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

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

</ off-topic-rant>


35
パフォーマンス上の理由でクラスを封印しません。私はデザイン上の理由でそれらを密封します。継承のための設計は困難であり、その努力はほとんどの場合無駄になります。ただし、インターフェースの提供については完全に同意します。これは、クラスを開封するためのはるかに優れたソリューションです。
Jon Skeet、

6
一般的に、カプセル化は継承よりも優れたソリューションです。特定のスレッドの例をとると、スレッドクラスのドキュメント化された動作を変更したため、スレッドの例外をトラップすると、リスコフ置換の原則が破られます。そのため、そこから派生できたとしても、SafeThreadをどこでも使用できるとは言えません。スレッドを使用できます。この場合、文書化された動作が異なる別のクラスにThreadをカプセル化することをお勧めします。時々、物事はあなたのために封印されます。
グレッグビーチ、

1
@ [Greg Beech]:事実ではなく意見-スレッドから継承してその設計の厄介な見落としを修正できることは悪いことではありません;-)そして、LSPを過大評価していると思います-証明可能なプロパティq(x)このケースは「未処理の例外がプログラムを破壊する」であり、これは「望ましいプロパティ」ではありません:-)
スティーブンA.ロウ

1
いいえ。ただし、他の開発者がコードを封印しないか、コーナーケースを許可することで悪用できるように、私は分からないコードを共有しています。最近の私のコードのほとんどは、アサーションやその他の契約関連のものです。そして、私がこれを行うのは、***の苦痛であるという事実についてはかなりオープンです。
チューリング

2
私たちはここで話題外の怒りをやっているので、あなたが封印されたクラスを嫌うように、私は飲み込まれた例外を嫌います。何かが爆乳になるときほど悪いことはありませんが、プログラムは継続します。JavaScriptは私のお気に入りです。一部のコードに変更を加え、突然ボタンをクリックしてもまったく何も起こりません。すごい!ASP.NETとUpdatePanelは別のものです。真剣に、私のボタンハンドラーがスローした場合、それはビッグディールであり、クラッシュする必要があるため、修正が必要なものがあることを知っています。何もしないボタンがあるより多くのクラッシュ画面が表示されますボタンよりも無用!
Roman Starkov 2013

4

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

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

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

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

(そして、はい、VMデザイナーは実際にこれらの小さなパフォーマンスの勝利を積極的に追求しています。)


3

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

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


彼らはすべきですが、そうではないようです。CLRがnull以外の参照型の概念を持っている場合、コンパイラはcall代わりにエミットすることができるため、シールされたクラスが実際に優れていcallvirtます...他の多くの理由からもnull以外の参照型が好きです...ため息:-(
Orion Edwards

3

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

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

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

そして、一番の理由:

b)私のクラスの悪用はそのようには不可能です

Microsoftが「封印された」標準ではなく「封印された」標準にしたかったのですが。


「除外する」(除外する)は「排出する」(作成する)べきだと思いますか?
user2864740

2

@Vaibhav、パフォーマンスを測定するためにどのようなテストを実行しましたか?

私は、Rotorを使用してCLIにドリルダウンし、密封されたクラスがパフォーマンスを向上させる方法を理解する必要があると思います。

SSCLI(ローター)
SSCLI:共有ソース共通言語インフラストラクチャ

共通言語インフラストラクチャ(CLI)は、.NET Frameworkのコアを記述するECMA標準です。共有ソースCLI(SSCLI)(Rotorとも呼ばれます)は、Microsoftの.NETアーキテクチャーの中心にあるテクノロジーであるECMA CLIおよびECMA C#言語仕様の実用的な実装へのソースコードの圧縮アーカイブです。


テストには、ダミーの作業(ほとんどの場合は文字列操作)を行っていたいくつかのメソッドを使用して、クラス階層を作成することが含まれていました。これらのメソッドのいくつかは仮想でした。彼らはあちこちでお互いを呼んでいた。次に、これらのメソッドを100、10000、100000回呼び出して...経過時間を測定します。次に、クラスを封印済みとしてマークした後、これらを実行します。そして再び測定します。それらに違いはありません。
Vaibhav、2011年

2

密封されたクラスは少なくとも少し高速になりますが、場合によってはwaayyy高速になる可能性があります... JITオプティマイザが仮想呼び出しである呼び出しをインライン化できる場合。そのため、インライン化するのに十分なほど小さい小さなメソッドがよくある場合は、クラスをシーリングすることを検討してください。

ただし、クラスをシールする最も良い理由は、「これを継承するように設計していないので、そうであると想定してあなたをやけどさせたりはしません。私はあなたがそれから派生させたので、実装に閉じ込められて自分を燃やすために」

私は、何人かから派生する機会が欲しいので、彼らが封印されたクラスを嫌うと言った人がいることを知っています...それ。「プライベートメンバーがいるクラスは嫌いです...アクセスできないため、クラスに思い通りの結果を出せないことがよくあります。」カプセル化は重要です...シーリングはカプセル化の一形態です。


C#コンパイラはcallvirt、シールされたクラスのインスタンスメソッドに対して(仮想メソッド呼び出し)を引き続き使用します。これは、クラスに対してnullオブジェクトチェックを実行する必要があるためです。インライン化に関して、CLR JITは、シールされたクラスとシールされていないクラスの両方の仮想メソッド呼び出しをインライン化できます(実際にインライン化します)。パフォーマンスは神話です。
オリオンエドワーズ

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(mov命令)に移動した後、密封されたメソッドを呼び出し、dword ptr [ecx]、ecx(cmp命令)の比較を実行し、派生/基本クラスがメソッドを直接実行しながらメソッドを呼び出します。 。

Torbj¨ornGranlundが書いたレポートによると、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
これにはいくつか問題があります。まず、最初のテストと2番目のテストの間でストップウォッチをリセットしていません。第二に、メソッドを呼び出す方法は、すべてのopコードがcallvirtではなく呼び出されることを意味するため、型は関係ありません。
Cameron MacFarland、

1
RuslanG、最初のテストを実行した後、watch.Reset()を呼び出すのを忘れました。o_O:]

はい、上のコードにはバグがあります。繰り返しになりますが、誰が自分のことを自慢して、コードの間違いを絶対に起こさないのでしょうか。 しかし、この答えには他のすべてのものよりも優れた重要なことが1つあります。問題の影響を測定しようとします。そして、この回答はまた、あなたが調べて[mass-] downvoteするすべての人のためにコードを共有します(大量のダウン投票がなぜ必要なのでしょうか?)。他の回答とは対照的です。私の意見ではそれは尊敬に値します...また、ユーザーは初心者なので、あまり歓迎的なアプローチではないことにも注意してください。いくつかの簡単な修正とこのコードは私たち全員にとって有用になります。修正+修正バージョンを共有する(勇気がある場合)
Roland Pihlakas

@RolandPihlakasに同意しません。あなたが言ったように、「誰が自分のことを自慢して、コードの間違いを決して起こさないか」。回答->いいえ、ありません。しかし、それは重要ではありません。ポイントは、別の新しいプログラマーを誤解させる可能性がある間違った情報です。多くの人は、ストップウォッチがリセットされなかったことに簡単に気付かないでしょう。彼らはベンチマーク情報が真実であると信じることができます。それはもっと有害ではありませんか?すぐに答えを探している人は、これらのコメントを読むことすらしないかもしれません。むしろ彼/彼女は答えを見て、彼/彼女はそれを信じるかもしれません。
Emran Hussain
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.