シールされたクラスは本当にパフォーマンス上の利点を提供しますか?

StackOverflow https://stackoverflow.com/questions/2134

質問

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

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

誰かがテストを実行して違いを確認したことがありますか?

勉強を手伝ってください:)

役に立ちましたか?

解決

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

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

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

これについて言及しているリンクが 1 つあります。 封印されたキーワードをとりとめなく語る

他のヒント

答えは「いいえ」です。シールされたクラスのパフォーマンスは、シールされていないクラスよりも優れているわけではありません。

問題は結局のところ、 callcallvirt IL オペコード。 Call よりも速いです callvirt, 、 そして callvirt は主に、オブジェクトがサブクラス化されているかどうかが不明な場合に使用されます。したがって、クラスをシールすると、すべてのオペコードが変更されると人々は考えます。 calvirtscalls そしてより速くなります。

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

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

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

呼び出しとcallvirt

アップデート:.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 

sealed がパフォーマンス上の利点があると言われる理由としてよく引用されるのは、コンパイラーはクラスがオーバーライドされていないことを認識しているため、 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

私にとって、これは、 できない シールされたクラスと非シールされたクラスでメソッドを呼び出す間のパフォーマンスの向上...今は幸せだと思います:-)

私が知っているように、パフォーマンス上の利点は保証されません。しかし〜がある 特定の条件下でパフォーマンスのペナルティを軽減するチャンス 密閉方式で。(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.


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

クラスをマークする sealed パフォーマンスに影響はないはずです。

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

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

JIT 側にとっては、ちょっとした賭けです。 後でロードされると、JIT はそのマシン コードを破棄してコードを再度コンパイルし、実際の仮想呼び出しを発行する必要があります。実際にはこのようなことはあまり起こらないと思います。

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

シールドされたクラス すべき パフォーマンスの向上を実現します。シールされたクラスは派生できないため、仮想メンバーは非仮想メンバーに変わる可能性があります。

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

<主題から外れた暴言>

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

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

</トピック外の暴言>

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

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

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

そして、主な理由は次のとおりです。

b) その方法では私の授業の悪用は不可能です

Microsoft は「アンシールド」ではなく「シールド」を標準にしてほしかったと思います。

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

使用する必要があると思います ローター また、CLI を詳しく調べて、シールされたクラスがどのようにパフォーマンスを向上させるかを理解します。

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

共通言語インフラストラクチャ(CLI)は、.NETフレームワークのコアを説明するECMA標準です。Rotorとしても知られる共有ソースCLI(SSCLI)は、ECMA CLIおよびECMA C#言語仕様の実装、Microsoftの.NETアーキテクチャの中心にあるテクノロジーの実用的な実装に対するソースコードの圧縮アーカイブです。

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

ただし、クラスを封印する最善の理由は、「私はこれを継承するように設計したわけではないので、そのように設計されていると仮定して火傷を負わせるつもりはありません。私はそのつもりはありません」と言うことです。実装から派生させたために実装に閉じ込められて火傷を負うのです。」

ここにいる何人かが、何かから派生する機会が欲しいので、シールドされたクラスが嫌いだと言っていることを私は知っています...しかし、それは多くの場合、最も保守しやすい選択ではありません...なぜなら、クラスを派生に公開すると、すべてを公開しないよりもはるかに多くのことが閉じ込められるからです。これは、「プライベート メンバーがいるクラスは嫌いです...」と言っているのと似ています。アクセス権がないため、クラスに希望どおりの動作をさせることができないことがよくあります。」 カプセル化は重要です...封止はカプセル化の 1 つの形式です。

このコードを実行すると、シールされたクラスが 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

ライセンス: CC-BY-SA帰属
所属していません StackOverflow
scroll top