О спектаклях unique_ptr - PullRequest
       33

О спектаклях unique_ptr

2 голосов
/ 15 ноября 2011

Я часто читаю, что 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 микросекунд

откуда разница? это объяснимо?

Ответы [ 3 ]

19 голосов
/ 10 октября 2012

ОБНОВЛЕН 01 января 2014 года

Я знаю, что этот вопрос довольно старый, но результаты все еще действительны для G ++ 4.7.0 и libstdc ++ 4.7.Итак, я попытался выяснить причину.

Здесь вы оцениваете производительность разыменования с использованием -O0 и, глядя на реализацию unique_ptr и shared_ptr, ваши результаты на самом деле верны.

unique_ptr сохраняет указатель и средство удаления в ::std::tuple, тогда как shared_ptr хранит непосредственно дескриптор обнаженного указателя.Таким образом, когда вы разыменовываете указатель (используя *, -> или get), у вас есть дополнительный вызов ::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(); }

ПРИМЕЧАНИЕ : Это не принципиальная проблема, которую можно легко устранить, отказавшись от использования:: std :: tuple в текущей реализации libstdc ++.

12 голосов
/ 15 ноября 2011

Все, что вы делали в таймерных блоках, это доступ к ним.Это не потребует дополнительных затрат.Увеличенное время, вероятно, связано с прокруткой вывода консоли.Вы никогда не сможете выполнить ввод-вывод в установленном времени.

И если вы хотите проверить накладные расходы на подсчет ссылок, то на самом деле выполните подсчет ссылок .Каким образом увеличенное время для строительства, уничтожения, назначения и других операций мутации shared_ptr будет вообще учитывать ваше время, если вы никогда не мутируете shared_ptr?

Редактировать:Если нет ввода / вывода, то где же оптимизация компилятора?Они должны были обстреляли все это.Даже идеоне не помогло.

3 голосов
/ 15 ноября 2011

Вы не тестируете здесь ничего полезного.

О чем вы говорите: копия

Что вы тестируете: итерация

Если вы хотите проверить копию, вам действительно нужно выполнить ее. Оба интеллектуальных указателя должны иметь одинаковую производительность, когда дело доходит до чтения, потому что хорошие реализации shared_ptr будут сохранять локальную копию объекта, на который указывает.

EDIT:

Относительно новых элементов:

В общем, даже не стоит говорить о скорости при использовании кода отладки. Если вы заботитесь о производительности, вы будете использовать код релиза (-O2 в целом) и, следовательно, это то, что должно быть измерено, так как между отладочным кодом и кодом релиза могут быть существенные различия. В частности, встраивание кода шаблона может серьезно сократить время выполнения.

Относительно эталона:

  • Я бы добавил еще один раунд мер: голые указатели. Обычно unique_ptr и голые указатели должны иметь одинаковую производительность, стоило бы проверить это, и это не обязательно должно быть верно в режиме отладки.
  • Возможно, вы захотите «чередовать» выполнение двух пакетов или, если не можете, взять среднее для каждого из нескольких прогонов. Таким образом, если во время завершения теста компьютер замедляется, будет затронута только партия unique_ptr, что приведет к нарушению измерения.

Возможно, вам будет интересно узнать у Нила больше: Радость тестов , это не исчерпывающее руководство, но оно довольно интересное. Особенно часть о том, как заставить побочные эффекты избегать удаления мертвого кода;)

Кроме того, будьте осторожны с измерениями. Разрешение ваших часов может быть менее точным, чем кажется. Например, если часы обновляются только каждые 15 часов, то любые меры около 15 часов являются подозрительными. При измерении кода выпуска может возникнуть проблема (вам может понадобиться добавить несколько витков в цикл).

...