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);
std::unique_ptr
にメモリオーバーヘッドがあるのは、重要な削除機能を提供した場合のみです。
std::shared_ptr
は常に非常に小さいですが、参照カウンターのメモリオーバーヘッドがあります。
std::unique_ptr
には、コンストラクター(提供された削除プログラムをコピーするか、ポインターをヌルで初期化する必要がある場合)およびデストラクター(所有オブジェクトを破棄する場合)のみに時間オーバーヘッドがあります。
std::shared_ptr
には、コンストラクター(参照カウンターを作成する)、デストラクター(参照カウンターをデクリメントし、オブジェクトを破棄する)、および代入演算子(参照カウンターをインクリメントする)に時間オーバーヘッドがあります。 std::shared_ptr
のスレッド安全性の保証により、これらの増分/減分はアトミックであるため、さらにオーバーヘッドが追加されます。
これらの操作はいずれも、参照解除(所有オブジェクトへの参照の取得)に時間オーバーヘッドがありませんが、この操作はポインターで最も一般的であるように見えることに注意してください。
要約すると、いくらかのオーバーヘッドがありますが、スマートポインターを継続的に作成および破棄しない限り、コードが遅くなることはありません。
すべてのコードパフォーマンスと同様に、ハード情報を取得するための本当に信頼できる唯一の手段は、measureおよび/またはinspectマシンコード。
そうは言っても、簡単な推論では
デバッグビルドでは、たとえば_operator->
_は、ステップインできるように関数呼び出しとして実行する必要があります(これは、クラスと関数を非デバッグとしてマークするサポートが一般的に不足しているためです)。
_shared_ptr
_の場合、制御ブロックの動的割り当てを伴う初期作成でいくらかのオーバーヘッドが期待できます。動的割り当ては、C++の他の基本操作よりも非常に遅いです(実際には_make_shared
_を使用してください) 、そのオーバーヘッドを最小限に抑えるため)。
また、_shared_ptr
_の場合、参照カウントを維持する際に最小限のオーバーヘッドがあります。値で_shared_ptr
_を渡す場合、_unique_ptr
_にはそのようなオーバーヘッドはありません。
上記の最初の点を念頭に置いて、測定するときは、デバッグビルドとリリースビルドの両方で行ってください。
国際C++標準化委員会は パフォーマンスに関するテクニカルレポート を公開しましたが、これは2006年の_unique_ptr
_および_shared_ptr
_が標準ライブラリに追加される前のことです。それでも、その時点ではスマートポインターは古臭かったので、レポートでもそれを考慮しました。関連部分の引用:
「単純なスマートポインターを介して値にアクセスするのが通常のポインターを介してアクセスするよりも大幅に遅い場合、コンパイラは抽象化を非効率的に処理しています。過去には、ほとんどのコンパイラーにはかなりの抽象化ペナルティがありましたが、現在のコンパイラーにはまだいくつかあります。ただし、少なくとも2つのコンパイラの抽象化ペナルティが1%未満、別のペナルティが3%であると報告されているため、この種のオーバーヘッドをなくすことは最新技術の範囲内です。
十分な情報に基づいた推測として、「最新技術の範囲内」は、2014年初頭時点で最も人気のあるコンパイラーで今日達成されています。
私の答えは他の答えとは異なり、彼らがコードをプロファイリングしたことは本当にあるのでしょうか。
shared_ptrには、制御ブロック(参照カウンタとすべての弱参照へのポインタリストを保持する)のメモリ割り当てのため、作成のための大きなオーバーヘッドがあります。また、これと、std :: shared_ptrは常に2ポインターTuple(オブジェクトに1つ、コントロールブロックに1つ)であるという事実のために、大きなメモリーオーバーヘッドもあります。
Shared_pointerを値パラメーターとして関数に渡すと、通常の呼び出しよりも少なくとも10倍遅くなり、スタックの巻き戻しのためにコードセグメントに多くのコードが作成されます。参照で渡すと、追加のインダイレクションが発生しますが、パフォーマンスの面でもかなり悪化する可能性があります。
そのため、関数が所有権管理に実際に関与していない限り、これを行うべきではありません。それ以外の場合は、「shared_ptr.get()」を使用します。通常の関数呼び出し中にオブジェクトが強制終了されないように設計されていません。
怒って、コンパイラの抽象構文ツリーのような小さなオブジェクトまたは他のグラフ構造の小さなノードでshared_ptrを使用すると、パフォーマンスが大幅に低下し、メモリが大幅に増加します。私は、C++ 14が市場に出てからすぐに、プログラマーがスマートポインターを正しく使用することを学ぶ前に書き直されたパーサーシステムを見てきました。書き換えは、古いコードよりも大幅に遅くなりました。
これは特効薬ではなく、生のポインターも定義上悪くありません。悪いプログラマーは悪く、悪いデザインは悪いです。慎重に設計し、明確な所有権を念頭に置いて設計し、ほとんどがサブシステムAPI境界でshared_ptrを使用するようにしてください。
さらに詳しく知りたい場合は、ニコライM.ジョスティスが「C++での共有ポインタの実際の価格」についての良い話を見ることができます https://vimeo.com/131189627
書き込みバリア、アトミックロックなどの実装の詳細とCPUアーキテクチャについて詳しく説明します。聞いた後は、この機能が安価であることは決して話さないでしょう。大きさの証明だけが必要な場合は、最初の48分間をスキップして、どこでも共有ポインターを使用するときに最大180倍(-O3でコンパイル)実行されるサンプルコードを実行しているのを見てください。
つまり、スマートポインターを使用するとコードは遅くなりますが、使用する場合はどれくらい遅くなりますか?
もっとゆっくり? shared_ptrsを使用して巨大なインデックスを作成していて、遠くから耐え難い力で老婦人が地面に落ちたように、コンピューターがしわになり始めるまでメモリが足りない場合を除きます。
コードを遅くするのは、検索の遅延、不要なループ処理、データの膨大なコピー、およびディスクへの大量の書き込み操作(数百など)です。
スマートポインターの利点はすべて管理に関連しています。 しかし、オーバーヘッドは必要ですか?これは実装に依存します。 3つのフェーズの配列を反復処理しており、各フェーズには1024個の要素の配列があるとします。このプロセスのsmart_ptr
を作成するのはやり過ぎかもしれません。反復が完了すると、それを消去する必要があることがわかるからです。したがって、smart_ptr
...を使用しないことで余分なメモリを獲得できます。
単一のメモリリークが原因で製品に障害が発生する可能性があります(プログラムが1時間ごとに4メガバイトをリークするとしましょう。コンピュータを破壊するには数か月かかりますが、それでも壊れます。 。
「ソフトウェアは3か月間保証されます。その後、サービスをご利用ください」と言っているようなものです。
結局、それは本当に問題です...このリスクに対処できますか?生のポインタを使用して何百もの異なるオブジェクトのインデックス作成を処理することは、メモリの制御を失う価値があります。
答えが「はい」の場合、生のポインターを使用します。
検討したくない場合でも、smart_ptr
は優れた実行可能な素晴らしいソリューションです。
ちらっと見るだけで、_[]
_演算子だけで、_gcc -lstdc++ -std=c++14 -O0
_を使用してコンパイルされ、この結果を出力した次のコードで示されるように、生のポインターよりも〜5X遅くなります:
_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;
}
_