Shared_ptrとC ++ 11のパフォーマンス:ライブラリを信じない理由

こんにちは

私はかつてコードの重要なセクションを最適化した後、ブーストがありました:: shared_ptr ...そして、気づきました。

カットの下の詳細。

そこで、コードを最適化したところ、次のようなセクションがありました。
auto pRes = boost :: static_pointer_cast < TBase > boost :: allocate_shared < TDerived > TAllocator )) ;
// ... pResで何かをする
return std :: move pRes

最適化が終わりに近づいたため、リリースがコンパイルされました。そこで、私がお気に入りのスタジオでコンパイルしたものを逆アセンブラーで調べて、美しく高速なものを期待することにしました。 それは私がショックを受けたのを見ただけです:
; -------------------------------------------------- -------------------------------------------
; 76行目:auto pRes = boost :: static_pointer_cast <CBase>(boost :: make_shared <CDerived>());

; ...面白くない-パラメータを準備する
コールブースト:: make_shared <CDerived> 0D211D0h
; ...再び興味深いことはありません-パラメータを準備します
呼び出しブースト:: static_pointer_cast <CBase CDerived> 0D212F0h
; ...繰り返しますが、興味深いことはありません-通話結果を受信する

; if(pRes)チェックのように見えますが、実際には問題ではありません。 jeが実行されないことが重要です
テスト eax eax
je `匿名名前空間` :: f + 7Ah 0D210CAh ; ->どこにもジャンプせず、pResがあります!= 0
; ...面白くない

; Epic fail#1-Interlocked Cmp Exchange
; このブロックは、結果として作成された一時的なshared_ptrを実際に削除します
; make_shared呼び出し:ここでは、参照カウントを減らしてから条件ジャンプを行います。
; 参照カウントがゼロでない場合に遷移が実行されます(明らかに、これはオプションです。
; なぜなら ポインターを作成しています)。
ロック xadd dword ptr [ eax ] ecx
jne `匿名名前空間` :: f + 7Ah 0D210CAh ; -> C ++コードの次の行にジャンプする

; ...潜在的なポインタ削除はまだありますが、デッドコードです

; -------------------------------------------------- -------------------------------------------
; 78行目:stdを返す:: move(pRes);

; アセンブラー、たぶん疲れました。
; このブロックでは、Epic Fail#2-Interlocked Incrementが最初に呼び出されます。 コピーする
; 値を返すpRes。 その後、Epic Fail#3-結果としてのインターロックされたCmp Exchange
; pResポインターの削除(メモリは自然に解放されません)

make_shared呼び出しとstatic_pointer_cast呼び出し内でさらに3つの連動命令について言及しなかったことを付け加えます。これを見て、目の前で悪化し始めました。 それはどういうことですか? ここでは具体的にムーブコンストラクターを呼び出していますが、参照カウントが前後にねじれていますか?

*叙情的な余談:何が悪いのか。
スマートポインタshared_ptrと呼ばれるものの中に、同じ格納オブジェクトを参照する共有ポインタの数へのポインタがあることは誰もが知っていると思います。 shared_ptrをコピーすると、この量は増加し、破棄すると減少します。 最後の共有ポインターの破棄中に、リンクの数はゼロになり、保存されたオブジェクトはそれに伴って削除されます。 したがって、これがマルチスレッド環境で正常に機能するには、アトミック操作、アセンブラーロックプレフィックスを持つリンクの数を変更する必要があります。このプレフィックスは、プロセッサが正確に正しく動作することを保証し、キャッシュは私たちの生活を妨げません。 プレフィックスは良いですが、遅いだけで、非常に遅いです。 チームを約2桁遅くします。 キャッシュラインのリセットが必要です。つまり、キャッシュラインをできるだけ使用する必要はありません。

*叙情的な余談2:どうして起こったのか、そしてなぜアトミックな指示がないのか。
C ++ 11は、移動セマンティクスと呼ばれる非常においしいものを提供してくれました。 これで、オブジェクトのコピーを作成する代わりに、オブジェクト間でデータを移動する「移動」コンストラクターを定義できます。 たとえば、このようなコンストラクタは、1つのstd :: stringから別のstd :: stringに内部文字列バッファへのポインタを移動し、メモリを再割り当てすることなく、あるオブジェクトから別のオブジェクトに文字列を移動できます。 同様に、参照カウンタを1つのshared_ptrから別のshared_ptrに移動することができます(そして必要です!)。 実際、この場合、ポインターの数を変更しないため、アトミック操作は必要ありません。 すべての内部データをあるデータから別のデータに「転送」するだけです(データを取得したポインタはどこにも表示されなくなります)。

どうしてそれが起こったのか...おそらく見落とされた。 ブーストで涙を流す手紙を書きたかったのですが、それをやり始めました...しかし、それから私は完全に私を打った何かを見つけました。 boost :: shared_ptrの作成中に、get_deleter関数はtypeid(oh gods!)を介して型比較を呼び出します。 私は彼らがそれをどのように持っているかわかりませんが、私のコンパイラはstrcmpを通してそれを行います(悲しいですね?)。

次に、ブーストと比較して標準ライブラリの速度を測定することにしました。 2回! boost :: make_sharedはstd :: make_sharedの2倍遅い! なぜですか? 簡単です。ブーストは、参照カウンターと実際に保存されたオブジェクトの2つのオブジェクトにメモリを割り当てます。 しかし、標準ライブラリ-1つだけ、このオブジェクトには両方が含まれています。 そして、メモリの割り当て-それは遅いです。 口頭プラスはマイクロソフトに行き、もう1つは標準ライブラリでスマートポインターが正常に機能するため、そこに到達しました-移動コンストラクターはアトミック操作を行いません。 ポインターの作成は、ロックフリーモードで行われます...まあ、ほぼ。 static_pointer_castを習得していませんでした。何が動いてもポインターをコピーします。 この問題は「dopilivanie」ライブラリによって解決されました。 別のプラットフォームには移植できませんが、標準と互換性があります。ここからダウンロードできます:pastebin.com/XZaE2cnW-MSVC2010で動作します。

PS

したがって、今日の勝者はMSVC2010の標準です。合計で1プラスです
しかし、ブーストは不運でした:-1

さてさようなら、少なくともこの情報が誰かに役立つことを願っています。 std :: shared_ptrを使用し、make / allocate sharedを使用してメモリを割り当ててください:)

Source: https://habr.com/ru/post/J138658/


All Articles