C ++の通常のポインターと比較して、スマートポインターのオーバーヘッドはどのくらいですか?


100

C ++ 11の通常のポインターと比較して、スマートポインターのオーバーヘッドはどのくらいですか?言い換えると、スマートポインターを使用するとコードが遅くなりますか?

具体的には、C ++ 11 std::shared_ptrとについて質問していstd::unique_ptrます。

明らかに、スタックを押し下げたものは大きくなります(少なくとも私はそう思います)。スマートポインターもその内部状態(参照カウントなど)を格納する必要があるため、問題は、実際にはどのくらいになるかです。パフォーマンスに影響がある場合はどうなりますか?

たとえば、通常のポインタではなく、関数からスマートポインタを返します。

std::shared_ptr<const Value> getValue();
// versus
const Value *getValue();

または、たとえば、関数の1つが通常のポインターの代わりにパラメーターとしてスマートポインターを受け入れる場合:

void setValue(std::shared_ptr<const Value> val);
// versus
void setValue(const Value *val);

8
知る唯一の方法は、コードをベンチマークすることです。
Basile Starynkevitch 14

どちらの意味ですか?std::unique_ptrまたはstd::shared_ptr
ステファン、2014年

10
答えは42です(別の言い方をすれば、コードのプロファイルを作成し、ハードウェアで一般的な作業負荷を理解する必要があります。)
Nim

アプリケーションで重要になるには、スマートポインタを極端に使用する必要があります。
user2672165 2014年

単純なセッター関数でshared_ptrを使用するコストはひどく、複数の100%オーバーヘッドが追加されます。
Lothar

回答:


176

std::unique_ptr いくつかの重要な削除機能を提供する場合にのみ、メモリのオーバーヘッドがあります。

std::shared_ptr 非常に小さいですが、参照カウンタのメモリオーバーヘッドは常にあります。

std::unique_ptr コンストラクター(提供された削除機能をコピーするか、ポインターをnull初期化する必要がある場合)とデストラクター(所有されているオブジェクトを破棄する)の間にのみ、オーバーヘッドが発生します。

std::shared_ptrコンストラクター(参照カウンターを作成するため)、デストラクター(参照カウンターをデクリメントし、オブジェクトを破壊する可能性がある)、および代入演算子(参照カウンターをインクリメントするため)に時間オーバーヘッドがあります。のスレッドセーフの保証によりstd::shared_ptr、これらのインクリメント/デクリメントはアトミックであるため、オーバーヘッドがさらに追加されます。

これらのいずれも、参照解除(所有オブジェクトへの参照の取得)に時間のオーバーヘッドがないことに注意してください。一方、この操作は、ポインターにとって最も一般的なようです。

要約すると、多少のオーバーヘッドがありますが、スマートポインターを継続的に作成して破棄しない限り、コードが遅くなることはありません。


11
unique_ptrデストラクタにはオーバーヘッドがありません。生のポインタを使用する場合とまったく同じです。
R.マルティーニョフェルナンデス2014

6
@ R.MartinhoFernandesは、生のポインタ自体と比較すると、生のポインタデストラクタは何もしないため、デストラクタで時間のオーバーヘッドがあります。生のポインタがおそらくどのように使用されるかと比較して、それは確かにオーバーヘッドがありません。
lisyarus 14

3
shared_ptrの構築/破壊/割り当てコストの一部がスレッドの安全性によるものであることに注目する価値がある
Joe

1
また、のデフォルトコンストラクタはstd::unique_ptrどうですか?を作成するstd::unique_ptr<int>と、内部int*nullptrあなたが好きかどうかに初期化されます。
Martin Drozdik、2016年

1
@MartinDrozdikほとんどの場合、生のポインタもnullで初期化し、後でnullかどうかを確認するか、またはそのようなことを行います。それにもかかわらず、これを回答に追加していただき、ありがとうございます。
lisyarus

26

すべてのコードパフォーマンスと同様に、ハード情報を取得するための本当に信頼できる唯一の方法は、マシンコードを測定または検査することです。

とはいえ、単純な推論では

  • たとえば、デバッグビルドではオーバーヘッドが発生する可能性がoperator->あります。これは、ステップインできるように関数呼び出しとして実行する必要があるためです(これは、クラスおよび関数を非デバッグとしてマークするためのサポートが一般的に不足しているためです)。

  • なぜならshared_ptr、制御ブロックの動的割り当てが含まれ、動的割り当てはC ++の他の基本的な操作よりも非常に遅いためです(make_shared実際に可能な場合は使用して、そのオーバーヘッドを最小限に抑えてください)。

  • またshared_ptr、たとえばshared_ptr値渡しの場合など、参照カウントの維持には最小限のオーバーヘッドがありますが、にはそのようなオーバーヘッドはありませんunique_ptr

上記の最初の点を念頭に置き、測定するときは、デバッグビルドとリリースビルドの両方でそれを行ってください。

国際C ++標準化委員会が発行している、パフォーマンス上の技術的な報告をする前に、が、これは2006年だったunique_ptrshared_ptr標準ライブラリに追加されました。それでも、スマートポインターはその時点では古くからあるので、レポートではそれも考慮しました。関連部分を引用:

「単純なスマートポインターを介して値にアクセスするのが通常のポインターを介してアクセスするよりも大幅に遅い場合、コンパイラーは抽象化を非効率的に処理しています。過去には、ほとんどのコンパイラーは抽象化に大きなペナルティを課し、現在のコンパイラーにはいくつかのペナルティがあります。ただし、少なくとも2つのコンパイラは、1%未満の抽象化ペナルティと3%のペナルティを持っていることが報告されているため、この種のオーバーヘッドを排除することは、最新の技術です。

情報に基づいた推測として、「最先端の技術」は、2014年の初めに現在最も人気のあるコンパイラで達成されました。


質問に追加したケースについて、回答に詳細を記載していただけませんか?
Venemo

これは10年以上前に当てはまったかもしれませんが、今日では、マシンコードの検査は上記の人が示唆するほど有用ではありません。命令がどのようにパイプライン化、ベクトル化されるか、そしてコンパイラー/プロセッサーが最終的に投機を処理する方法に応じて、それはどれほど高速かです。マシンコードのコードが少ないからといって、必ずしもコードが高速であるとは限りません。パフォーマンスを決定する唯一の方法は、それをプロファイルすることです。これは、プロセッサごとに、またコンパイラごとに変わる可能性があります。
Byron

私が見た問題は、shared_ptrsがサーバーで使用されると、shared_ptrsの使用が急増し始め、すぐにshared_ptrsがデフォルトのメモリ管理手法になることです。つまり、今度は1〜3%の抽象化ペナルティが繰り返され、それが何度も繰り返されます。
Nathan Doromal

デバッグビルドのベンチマークは完全で時間の無駄です
ポールチャイルズ

26

私の答えは他の人とは異なり、彼らがコードをプロファイリングしたことは本当にあるのでしょうか。

shared_ptrは、制御ブロック(参照カウンターとすべての弱い参照へのポインターリストを保持する)へのメモリ割り当てのため、作成にかなりのオーバーヘッドがあります。これとstd :: shared_ptrは常に2ポインターのタプル(1つはオブジェクト、もう1つは制御ブロック)であるため、メモリオーバーヘッドが非常に大きくなります。

shared_pointerを値パラメーターとして関数に渡すと、通常の呼び出しよりも少なくとも10倍遅くなり、スタックを巻き戻すためのコードセグメントに多くのコードが作成されます。参照渡しする場合、追加の間接参照が発生しますが、これもパフォーマンスの点でかなり悪い可能性があります。

そのため、機能が実際に所有権の管理に関与していない限り、これを行うべきではありません。それ以外の場合は、「shared_ptr.get()」を使用します。通常の関数呼び出し中にオブジェクトが強制終了されないようにするためのものではありません。

気が狂って、コンパイラの抽象構文ツリーのような小さなオブジェクトや他のグラフ構造の小さなノードでshared_ptrを使用すると、パフォーマンスが大幅に低下し、メモリが大幅に増加します。C ++ 14が市場に出た直後で、プログラマーがスマートポインターを正しく使用することを学ぶ前に、パーサーシステムを書き直しました。書き換えは、古いコードよりも大幅に遅くなりました。

これは特効薬ではなく、生のポインタも定義上は悪くありません。悪いプログラマは悪いし、悪いデザインは悪い。慎重に設計し、明確な所有権を念頭に置いて設計し、ほとんどの場合サブシステムAPI境界でshared_ptrを使用するようにしてください。

詳細については、Nicolai M. Josuttisの「C ++での共有ポインタの実際の価格」についての良い話をご覧ください。
ロックなど、一度聞いてみると、この機能が安いという話は決してありません。マグニチュードが遅いことを証明したいだけの場合は、最初の48分をスキップして、どこでも共有ポインターを使用したときに実行速度が最大180倍になるサンプルコード(-O3でコンパイル)を実行しているのを見てください。


ご回答有難うございます!どのプラットフォームでプロファイルしましたか?クレームをいくつかのデータでバックアップできますか?
Venemo

表示する番号はありませんが、ニコ・ジョスティスの講演vimeo.com/131189627
Lothar

6
聞いたことあるstd::make_shared()?また、露骨な誤用のデモンストレーションは少し退屈だと思います...
デデュプリケーター

2
「make_shared」が実行できることはすべて、1つの追加の割り当てから安全であり、制御ブロックがオブジェクトの前に割り当てられている場合は、キャッシュの局所性を少し増やします。ポインタを回しても何の役にも立ちません。これは問題の原因ではありません。
Lothar 2017

14

言い換えると、スマートポインターを使用するとコードが遅くなりますか?

もっとゆっくり?おそらく、そうではありません。shared_ptrsを使用して巨大なインデックスを作成し、コンピューターがしわを寄せ始めるのに十分なメモリがない場合は、おばあさんが遠くから耐えられない力で地面に落ち込んだ場合などです。

コードが遅くなるのは、検索が遅い、不要なループ処理、大量のデータのコピー、ディスクへの大量の書き込み操作(数百など)です。

スマートポインターの利点はすべて管理に関連しています。しかし、オーバーヘッドは必要ですか?これは実装によって異なります。3つのフェーズの配列を反復処理しているとします。各フェーズには1024個の要素の配列があります。smart_ptrこのプロセスのを作成するのはやり過ぎかもしれません。反復が完了すると、それを消去する必要があることがわかるからです。したがって、使用しないことで追加のメモリを獲得できますsmart_ptr...

しかし、本当にそうしたいですか?

単一のメモリリークにより、製品に特定の時点で障害が発生する可能性があります(プログラムが毎時4メガバイトをリークするとします。コンピュータが壊れるまでに数か月かかりますが、それでも壊れます。リークが存在するのでわかっています)。 。

「ソフトウェアは3か月間保証されているので、サービスを受けるには私に電話してください」のようなものです。

結局のところ、それは本当に問題です...このリスクに対処できますか?生のポインタを使用して何百もの異なるオブジェクトのインデックスを処理することは、メモリの制御を失う価値があります。

答えが「はい」の場合、生のポインタを使用します。

それを考慮したくない場合でも、a smart_ptrは実行可能で優れた優れたソリューションです。


4
[OK]を、しかし、valgrindのはいい ™ので、限り、あなたはそれを使用すると、あなたが安全であるべき、可能なメモリリークのチェックで
graywolf

@Paladinはい、あなたの記憶を処理することができればsmart_ptr、大規模なチームにとって本当に役に立ちます
Claudiordgz '10

3
私はunique_ptrを使用しています。多くのことを簡素化しますが、shared_ptrは好きではありません。参照カウントは非常に効率的なGCではなく、完璧でもありません
graywolf

1
@Paladinすべてをカプセル化できる場合は、生のポインタを使用しようとします。それが引数のようにあちこちに渡されるものである場合は、おそらくsmart_ptrを検討します。私unique_ptrsのほとんどは、メインまたは実行する方法のように、大きな実装で使用されている
Claudiordgz

@Lothar私があなたの答えで私が言ったことの1つを言い換えたのを見ます:Thats why you should not do this unless the function is really involved in ownership management...すばらしい答え、ありがとう、賛成
Claudiordgz

0

[]次のコードで示されているようにコンパイルしgcc -lstdc++ -std=c++14 -O0てこの結果を出力した次のコードに示されているように、一見と演算子だけで、生のポインタよりも5倍遅くなります。

malloc []:     414252610                                                 
unique []  is: 2062494135                                                
uq get []  is: 238801500                                                 
uq.get()[] is: 1505169542
new is:        241049490 

私はc ++を学び始めています。これは私の頭の中にあります。あなたは常にあなたが何をしているかを知る必要があり、他の人があなたのc ++で何をしたかを知るためにより多くの時間をかける必要があります。

編集

@Mohan Kumarによって言及されたように、私は詳細を提供しました。gccのバージョンは7.4.0 (Ubuntu 7.4.0-1ubuntu1~14.04~ppa1)です。上記の結果-O0はを使用したときに取得されましたが、「-O2」フラグを使用すると、次の結果が得られました。

malloc []:     223
unique []  is: 105586217
uq get []  is: 71129461
uq.get()[] is: 69246502
new is:        9683

その後に移行しclang version 3.9.0-O0されました:

malloc []:     409765889
unique []  is: 1351714189
uq get []  is: 256090843
uq.get()[] is: 1026846852
new is:        255421307

-O2 だった:

malloc []:     150
unique []  is: 124
uq get []  is: 83
uq.get()[] is: 83
new is:        54

clangの結果-O2は驚くべきものです。

#include <memory>
#include <iostream>
#include <chrono>
#include <thread>

uint32_t n = 100000000;
void t_m(void){
    auto a  = (char*) malloc(n*sizeof(char));
    for(uint32_t i=0; i<n; i++) a[i] = 'A';
}
void t_u(void){
    auto a = std::unique_ptr<char[]>(new char[n]);
    for(uint32_t i=0; i<n; i++) a[i] = 'A';
}

void t_u2(void){
    auto a = std::unique_ptr<char[]>(new char[n]);
    auto tmp = a.get();
    for(uint32_t i=0; i<n; i++) tmp[i] = 'A';
}
void t_u3(void){
    auto a = std::unique_ptr<char[]>(new char[n]);
    for(uint32_t i=0; i<n; i++) a.get()[i] = 'A';
}
void t_new(void){
    auto a = new char[n];
    for(uint32_t i=0; i<n; i++) a[i] = 'A';
}

int main(){
    auto start = std::chrono::high_resolution_clock::now();
    t_m();
    auto end1 = std::chrono::high_resolution_clock::now();
    t_u();
    auto end2 = std::chrono::high_resolution_clock::now();
    t_u2();
    auto end3 = std::chrono::high_resolution_clock::now();
    t_u3();
    auto end4 = std::chrono::high_resolution_clock::now();
    t_new();
    auto end5 = std::chrono::high_resolution_clock::now();
    std::cout << "malloc []:     " <<  (end1 - start).count() << std::endl;
    std::cout << "unique []  is: " << (end2 - end1).count() << std::endl;
    std::cout << "uq get []  is: " << (end3 - end2).count() << std::endl;
    std::cout << "uq.get()[] is: " << (end4 - end3).count() << std::endl;
    std::cout << "new is:        " << (end5 - end4).count() << std::endl;
}

コードをテストしましたが、一意のポインタを使用した場合の速度はわずか10%です。
Mohan Kumar

8
-O0コードでベンチマークしたり、コードをデバッグしたりすることはありません。出力は非常に非効率的です。常に少なくとも使用してください-O2(または-O3、一部のベクトル化がで行われていないため、現在では使用しています-O2
phuclv

1
時間があり、コーヒーブレークが必要な場合は、-O4を使用してリンク時の最適化を行うと、小さなすべての小さな抽象化関数がインラインで消えます。
ローター・

sが内部でデストラクタで呼び出しているため、freemallocテストとdelete[]new(または変数をa静的にする)の呼び出しを含める必要があります。unique_ptrdelete[]
RnMss
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.