3

unique_ptr はコピー不可で移動セマンティクスがあるため、ほとんどの状況で shared_ptr よりも unique_ptr が好まれるとよく​​読みます。shared_ptr は、コピーと参照カウントによるオーバーヘッドを追加します。

しかし、いくつかの状況でunique_ptrをテストすると、対応するものよりも(アクセスが)著しく遅いようです

たとえば、gcc 4.5では次のようになります。

edit : print メソッドは実際には何も印刷しません

#include <iostream>
#include <string>
#include <memory>
#include <chrono>
#include <vector>

class Print{

public:
void print(){}

};

void test()
{
 typedef vector<shared_ptr<Print>> sh_vec;
 typedef vector<unique_ptr<Print>> u_vec;

 sh_vec shvec;
 u_vec  uvec;

 //can't use initializer_list with unique_ptr
 for (int var = 0; var < 100; ++var) {

    shared_ptr<Print> p(new Print());
    shvec.push_back(p);

    unique_ptr<Print> p1(new Print());
    uvec.push_back(move(p1));

  }

 //-------------test shared_ptr-------------------------
 auto time_sh_1 = std::chrono::system_clock::now();

 for (auto var = 0; var < 1000; ++var) 
 {
   for(auto it = shvec.begin(), end = shvec.end(); it!= end; ++it)
   {
     (*it)->print();
   }
 }

 auto time_sh_2 = std::chrono::system_clock::now();

 cout <<"test shared_ptr : "<< (time_sh_2 - time_sh_1).count() << " microseconds." << endl;

 //-------------test unique_ptr-------------------------
 auto time_u_1 = std::chrono::system_clock::now();

 for (auto var = 0; var < 1000; ++var) 
 {
   for(auto it = uvec.begin(), end = uvec.end(); it!= end; ++it)
   {
     (*it)->print();
   }
 }

 auto time_u_2 = std::chrono::system_clock::now();

 cout <<"test unique_ptr : "<< (time_u_2 - time_u_1).count() << " microseconds." << endl;

}

平均して (g++ -O0) を取得します。

  • shared_ptr : 1480 マイクロ秒
  • unique_ptr : 3350 マイクロ秒

違いはどこから来るのですか?それは説明可能ですか?

4

3 に答える 3

22

2014 年 1 月 1 日更新

この質問はかなり古いものですが、結果は G++ 4.7.0 および libstdc++ 4.7 でも有効です。というわけで、その理由を調べてみました。

ここでベンチマークしているのは、-O0を使用した逆参照のパフォーマンスです。と の実装を見ると、結果は実際には正しいです。unique_ptrshared_ptr

unique_ptrポインタとデリータを に::std::tuple格納shared_ptrし、ネイキッド ポインタ ハンドルを直接格納します。そのため、(*、->、または get を使用して) ポインターを逆参照すると、 in への余分な呼び出しが発生::std::get<0>()unique_ptrます。対照的にshared_ptr、ポインタを直接返します。gcc-4.7 では、最適化してインライン化した場合でも、::std::get<0>() は直接ポインターよりも少し遅くなります。. 最適化してインライン化すると、gcc-4.8.1 は ::std::get<0>() のオーバーヘッドを完全に省略します。私のマシンでは、 でコンパイルすると-O3、コンパイラはまったく同じアセンブリ コードを生成します。つまり、文字通り同じです。

全体として、現在の実装を使用するshared_ptrと、作成、移動、コピー、および参照カウントが遅くなりますが、 *逆参照*では同じくらい高速です。

print()質問では空であり、コンパイラは最適化時にループを省略します。そこで、最適化の結果を正しく観察するためにコードを少し変更しました。

#include <iostream>
#include <string>
#include <memory>
#include <chrono>
#include <vector>

using namespace std;

class Print {
 public:
  void print() { i++; }

  int i{ 0 };
};

void test() {
  typedef vector<shared_ptr<Print>> sh_vec;
  typedef vector<unique_ptr<Print>> u_vec;

  sh_vec shvec;
  u_vec uvec;

  // can't use initializer_list with unique_ptr
  for (int var = 0; var < 100; ++var) {
    shvec.push_back(make_shared<Print>());
    uvec.emplace_back(new Print());
  }

  //-------------test shared_ptr-------------------------
  auto time_sh_1 = std::chrono::system_clock::now();

  for (auto var = 0; var < 1000; ++var) {
    for (auto it = shvec.begin(), end = shvec.end(); it != end; ++it) {
      (*it)->print();
    }
  }

  auto time_sh_2 = std::chrono::system_clock::now();

  cout << "test shared_ptr : " << (time_sh_2 - time_sh_1).count()
       << " microseconds." << endl;

  //-------------test unique_ptr-------------------------
  auto time_u_1 = std::chrono::system_clock::now();

  for (auto var = 0; var < 1000; ++var) {
    for (auto it = uvec.begin(), end = uvec.end(); it != end; ++it) {
      (*it)->print();
    }
  }

  auto time_u_2 = std::chrono::system_clock::now();

  cout << "test unique_ptr : " << (time_u_2 - time_u_1).count()
       << " microseconds." << endl;
}

int main() { test(); }

: これは根本的な問題ではなく、現在の libstdc++ 実装で ::std::tuple の使用を破棄することで簡単に修正できます。

于 2012-10-10T00:00:14.197 に答える
12

時限ブロックで行ったのは、それらにアクセスすることだけです。これにより、追加のオーバーヘッドはまったく発生しません。増加した時間は、おそらくコンソール出力のスクロールによるものです。時間制限のあるベンチマークで I/O を実行することは決してできません。

また、参照カウントのオーバーヘッドをテストしたい場合は、実際に参照カウントを実行してください決して変異しない場合、建設、破壊、割り当て、およびその他の変異操作の増加した時間は、shared_ptrあなたの時間にどのように影響しますか?shared_ptr

編集: I/O がない場合、コンパイラの最適化はどこにありますか? 彼らはすべてを核攻撃すべきだった。ideone でさえ、多くのことをジャンクしました。

于 2011-11-15T15:07:16.480 に答える
3

ここでは、有用なものをテストしていません。

あなたが話していること:コピー

何をテストしているか:反復

テストコピーを行う場合は、実際にコピーを実行する必要があります。両方のスマート ポインターは、読み取りに関しては同様のパフォーマンスを持つ必要があります。これは、適切なshared_ptr実装では、指しているオブジェクトのローカル コピーが保持されるためです。

編集:

新要素について:

一般に、デバッグ コードを使用する場合の速度について話す価値さえありません。パフォーマンスを重視する場合は、リリース コード (-O2一般的に) を使用します。デバッグ コードとリリース コードの間には大きな違いがある可能性があるため、これを測定する必要があります。最も注目すべきは、テンプレート コードのインライン化によって、実行時間が大幅に短縮される可能性があることです。

ベンチマークについて:

  • 別の対策を追加します: ネイキッド ポインターです。通常、unique_ptrネイキッド ポインターのパフォーマンスは同じである必要があり、チェックする価値があり、デバッグ モードで必ずしも true である必要はありません。
  • 2 つのバッチの実行を「インターリーブ」するか、それができない場合は、いくつかの実行間でそれぞれの平均を取ります。そのままでは、ベンチマークの終了時にコンピューターの速度が低下した場合、unique_ptr影響を受けるのはバッチのみであり、測定に影響を与えます。

Neil: The Joy of Benchmarksからさらに学ぶことに興味があるかもしれません。これは決定的なガイドではありませんが、非常に興味深いものです。特に、デッドコードの削除を回避するために副作用を強制することに関する部分;)

また、測り方にも注意が必要です。時計の解像度は、見た目よりも正確ではない場合があります。たとえば、クロックが 15us ごとにのみ更新される場合、15us 前後の測定値は疑わしいものです。これは、リリース コードを測定するときに問題になる可能性があります (ループに数ターンを追加する必要がある場合があります)。

于 2011-11-15T15:09:39.553 に答える