パフォーマンスをさらに向上させるには、クラスをシール済みとしてマークする必要があるという、多くの最適化のヒントを見つけました。
パフォーマンスの違いを確認するためにいくつかのテストを実行したところ、何も見つかりませんでした。私は何か間違ったことをしていますか?密封されたクラスがより良い結果をもたらすケースを見逃していますか?
誰かがテストを実行して違いを見ましたか?
学ぶのを手伝ってください:)
パフォーマンスをさらに向上させるには、クラスをシール済みとしてマークする必要があるという、多くの最適化のヒントを見つけました。
パフォーマンスの違いを確認するためにいくつかのテストを実行したところ、何も見つかりませんでした。私は何か間違ったことをしていますか?密封されたクラスがより良い結果をもたらすケースを見逃していますか?
誰かがテストを実行して違いを見ましたか?
学ぶのを手伝ってください:)
回答:
JITterは、シールされたクラス内のメソッドへの非仮想呼び出しを使用することがあります。これは、それらをさらに拡張する方法がないためです。
呼び出しタイプ、仮想/非仮想に関する複雑なルールがあり、それらすべてを知っているわけではないので、それらの概要を説明することはできませんが、シールされたクラスと仮想メソッドをググると、トピックに関するいくつかの記事が見つかるかもしれません。
このレベルの最適化から得られるパフォーマンスの利点はすべて、最後の手段と見なす必要があります。コードレベルで最適化する前に、常にアルゴリズムレベルで最適化してください。
:ここでは、この言及一つのリンクだ密封されたキーワードのランブリングは、
答えはノーです。密封されたクラスは、密封されていないクラスよりもパフォーマンスが良くありません。
問題はcallvs callvirtILオペコードに帰着します。Callはよりも高速でcallvirt、callvirt主にオブジェクトがサブクラス化されているかどうかが不明な場合に使用されます。したがって、人々は、クラスを封印すると、すべてのopコードがからcalvirtsに変わり、calls高速になると想定します。
残念ながらcallvirt、null参照のチェックなど、他にも便利な機能があります。これは、クラスがシールされている場合でも、参照がnullである可能性があるため、a callvirtが必要であることを意味します。(クラスを封印する必要なく)これを回避することはできますが、少し無意味になります。
call構造体は、サブクラス化できず、nullになることがないために使用されます。
詳細については、この質問を参照してください。
call使用される状況は次のとおりです。状況new T().Method()、structメソッドの場合、メソッドへの非仮想呼び出しvirtual(などbase.Virtual())、またはstaticメソッドの場合。他のどこでも使用しcallvirtます。
                    更新:.NET Core 2.0および.NET Desktop 4.7.1以降、CLRは非仮想化をサポートするようになりました。シールクラスのメソッドを取得し、仮想呼び出しを直接呼び出しに置き換えることができます。また、安全であることがわかっている場合は、非シールクラスに対してもこれを行うことができます。
そのような場合(CLRが仮想化しても安全であるとCLRが検出できなかった封印されたクラス)、封印されたクラスは、実際にはある種のパフォーマンス上の利点を提供するはずです。
とは言っても、コードをプロファイリングして、何百万回も呼び出されている特にホットなパスにいると判断しない限り、心配する価値はないと思います。
元の回答:
次のテストプログラムを作成し、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
私にとって、これは、シールされたクラスとシールされていないクラスでメソッドを呼び出す間にパフォーマンスの向上があり得ないという確かな証拠を提供します...私は今幸せだと思います:-)
callvirtこれらのメソッドが最初から仮想化されていないのになぜそこにあるのですか?
                    callvirt、メソッド呼び出しを呼び出す前にオブジェクトをnullチェックする必要があるため、シールされたメソッドにCLRが使用されていることを読みました。使用しますcallvirt。を削除してcallvirt直接ジャンプするに((string)null).methodCall()は、C ++のようにC#を変更できるようにするか、オブジェクトがnullではないことを静的に証明する必要があります(これは可能ですが、気にしていません)。
                    私が知っているように、パフォーマンス上の利点の保証はありません。ただし、特定の条件下でパフォーマンスのペナルティを減らす可能性があります、密封された方法を使用とでます。(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。
この答え(ソートされていない配列よりもソートされた配列を処理する方が速いのはなぜですか?)は、分岐予測をよりよく説明しています。
<off-topic-rant>
私 密封されたクラス嫌います。パフォーマンスの利点が驚異的であるとしても(疑わしいと思います)、継承による再利用を防ぐことでオブジェクト指向モデルを破壊します。たとえば、Threadクラスはシールされています。スレッドをできるだけ効率的にしたいと思うかもしれませんが、Threadをサブクラス化できることで大きなメリットが得られるシナリオも想像できます。クラスの作成者は、「パフォーマンス」の理由でクラスをシールする必要がある場合は、少なくともインターフェイスを提供してください。忘れた機能が必要なすべての場所で折り返し置換する必要はありません。
例:SafeThread Threadがシールされており、IThreadインターフェイスがないため、はThreadクラスをラップする必要がありました。SafeThreadは、スレッドで未処理の例外を自動的にトラップします。これは、Threadクラスから完全に失われたものです。[いいえ、未処理の例外イベントは、セカンダリスレッドの未処理の例外を取得しません ]。
</ off-topic-rant>
クラスをマークする sealedをても、パフォーマンスに影響はありません。
オペコードの代わりにオペコードを発行cscする必要がある場合があります。ただし、そのようなケースはまれです。callvirtcall
そしてJITが同じ非仮想関数呼び出しを放出することができるべきであると私には思えるのためにcallvirt、それがためになることcall、それは(まだ)クラスは、任意のサブクラスを持っていないことを知っている場合。メソッドの実装が1つしかない場合は、vtableからそのアドレスをロードしても意味がありません。1つの実装を直接呼び出すだけです。さらに言えば、JITは関数をインライン化することもできます。
それはJITの部分で少しギャンブルです。なぜなら、サブクラスが後でロードされると、JITはそのマシンコードを破棄してコードを再度コンパイルし、実際の仮想呼び出しを発行する必要があるためです。私の推測では、これは実際には頻繁には起こりません。
(そして、はい、VMデザイナーは実際にこれらの小さなパフォーマンスの勝利を積極的に追求しています。)
シールされたクラスは、パフォーマンスを向上させるはずです。封印されたクラスは派生できないため、仮想メンバーを非仮想メンバーに変えることができます。
もちろん、私達は本当に小さな利益について話している。プロファイリングで問題であることが明らかにならない限り、パフォーマンスを向上させるためだけにクラスをシール済みとしてマークしません。
call代わりにエミットすることができるため、シールされたクラスが実際に優れていcallvirtます...他の多くの理由からもnull以外の参照型が好きです...ため息:-(
                    私は「密封された」クラスを通常の場合と考えており、「密封された」キーワードを省略する理由は常にあります。
私にとって最も重要な理由は次のとおりです。
a)より良いコンパイル時間チェック(実装されていないインターフェースへのキャストは、実行時だけでなく、コンパイル時にも検出されます)
そして、一番の理由:
b)私のクラスの悪用はそのようには不可能です
Microsoftが「封印された」標準ではなく「封印された」標準にしたかったのですが。
@Vaibhav、パフォーマンスを測定するためにどのようなテストを実行しましたか?
私は、Rotorを使用してCLIにドリルダウンし、密封されたクラスがパフォーマンスを向上させる方法を理解する必要があると思います。
SSCLI(ローター)
SSCLI:共有ソース共通言語インフラストラクチャ共通言語インフラストラクチャ(CLI)は、.NET Frameworkのコアを記述するECMA標準です。共有ソースCLI(SSCLI)(Rotorとも呼ばれます)は、Microsoftの.NETアーキテクチャーの中心にあるテクノロジーであるECMA CLIおよびECMA C#言語仕様の実用的な実装へのソースコードの圧縮アーカイブです。
密封されたクラスは少なくとも少し高速になりますが、場合によってはwaayyy高速になる可能性があります... JITオプティマイザが仮想呼び出しである呼び出しをインライン化できる場合。そのため、インライン化するのに十分なほど小さい小さなメソッドがよくある場合は、クラスをシーリングすることを検討してください。
ただし、クラスをシールする最も良い理由は、「これを継承するように設計していないので、そうであると想定してあなたをやけどさせたりはしません。私はあなたがそれから派生させたので、実装に閉じ込められて自分を燃やすために」
私は、何人かから派生する機会が欲しいので、彼らが封印されたクラスを嫌うと言った人がいることを知っています...それ。「プライベートメンバーがいるクラスは嫌いです...アクセスできないため、クラスに思い通りの結果を出せないことがよくあります。」カプセル化は重要です...シーリングはカプセル化の一形態です。
callvirt、シールされたクラスのインスタンスメソッドに対して(仮想メソッド呼び出し)を引き続き使用します。これは、クラスに対してnullオブジェクトチェックを実行する必要があるためです。インライン化に関して、CLR JITは、シールされたクラスとシールされていないクラスの両方の仮想メソッド呼び出しをインライン化できます(実際にインライン化します)。パフォーマンスは神話です。
                    それらを実際に見るには、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での次の命令の速度です。
リンク:https : //gmplib.org/~tege/x86-timing.pdf
つまり、理想的には、シールドされたメソッドを呼び出すのに必要な時間は2サイクルであり、派生または基本クラスのメソッドを呼び出すのに必要な時間は3サイクルです。
コンパイラーの最適化により、封印されたクラスと封印されていないクラスのパフォーマンスの差が非常に小さくなり、プロセッサーサークルについて話しているため、大部分のアプリケーションには関係ありません。
このコードを実行すると、シールされたクラスが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