Стоимость проезда по shared_ptr - PullRequest
55 голосов
/ 23 марта 2010

Я широко использую std :: tr1 :: shared_ptr в своем приложении.Это включает передачу объектов в качестве аргументов функции.Рассмотрим следующее:

class Dataset {...}

void f( shared_ptr< Dataset const > pds ) {...}
void g( shared_ptr< Dataset const > pds ) {...}
...

Хотя передача объекта набора данных через shared_ptr гарантирует его существование внутри f и g, функции могут вызываться миллионы раз, что вызывает создание и уничтожение большого количества объектов shared_ptr.Вот фрагмент плоского профиля gprof из последнего запуска:

Each sample counts as 0.01 seconds.
  %   cumulative   self              self     total
 time   seconds   seconds    calls   s/call   s/call  name
  9.74    295.39    35.12 2451177304     0.00     0.00  std::tr1::__shared_count::__shared_count(std::tr1::__shared_count const&)
  8.03    324.34    28.95 2451252116     0.00     0.00  std::tr1::__shared_count::~__shared_count()

Итак, ~ 17% времени выполнения было потрачено на подсчет ссылок с объектами shared_ptr.Это нормально?

Большая часть моего приложения однопоточная, и я думал о переписывании некоторых функций как

void f( const Dataset& ds ) {...}

и замене вызовов

shared_ptr< Dataset > pds( new Dataset(...) );
f( pds );

с

f( *pds );

в местах, где я точно знаю, что объект не будет уничтожен, пока поток программы находится внутри f ().Но прежде чем я убежал, чтобы изменить группу сигнатур / вызовов функций, я хотел знать, каков был типичный удар по производительности при передаче shared_ptr.Похоже, что shared_ptr не следует использовать для функций, которые вызываются очень часто.

Любой вклад приветствуется.Спасибо за чтение.

-Artem

Обновление: После изменения нескольких функций для принятия const Dataset& новый профиль выглядит следующим образом:

Each sample counts as 0.01 seconds.
  %   cumulative   self              self     total
 time   seconds   seconds    calls   s/call   s/call  name
  0.15    241.62     0.37 24981902     0.00     0.00  std::tr1::__shared_count::~__shared_count()
  0.12    241.91     0.30 28342376     0.00     0.00  std::tr1::__shared_count::__shared_count(std::tr1::__shared_count const&)

Я немного озадачен тем, что количество вызовов деструкторов меньше количества вызовов конструктора копирования, но в целом я очень рад уменьшению связанного времени выполнения.Спасибо всем за советы.

Ответы [ 5 ]

56 голосов
/ 23 марта 2010

Всегда передавайте shared_ptr по const ссылка:

void f(const shared_ptr<Dataset const>& pds) {...} 
void g(const shared_ptr<Dataset const>& pds) {...} 

Редактировать: Относительно вопросов безопасности, упомянутых другими:

  • При интенсивном использовании shared_ptr во всем приложении передача по значению отнимает огромное количество времени (я видел, что он составляет 50 +%).
  • Используйте const T& вместо const shared_ptr<T const>&, когда аргумент не должен быть нулевым.
  • Использование const shared_ptr<T const>& безопаснее, чем const T*, когда производительность является проблемой.
10 голосов
/ 23 марта 2010

Вам нужен shared_ptr только для передачи его функциям / объектам, которые хранят его для будущего использования. Например, некоторые классы могут хранить shared_ptr для использования в рабочем потоке. Для простых синхронных вызовов вполне достаточно использовать простой указатель или ссылку. shared_ptr не должен полностью заменять простые указатели.

5 голосов
/ 23 марта 2010

Если вы не используете make_shared , не могли бы вы попробовать? Располагая счетчик ссылок и объект в одной и той же области памяти, вы можете увидеть увеличение производительности, связанное с когерентностью кэша. В любом случае стоит попробовать.

3 голосов
/ 23 марта 2010

В приложениях, критичных к производительности, следует избегать создания и уничтожения любых объектов, особенно создания и уничтожения избыточных объектов.

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

Предположительно, вам нужен shared_ptr (потому что, если вам удастся избежать локального объекта, вы не выделите один из кучи), но вы даже можете "кэшировать" результат разыменования shared_ptr:

void fn(shared_ptr< Dataset > pds)
{
   Dataset& ds = *pds;

   for (i = 0; i < 1000; ++i)
   {
      f(ds);
      g(ds);
   }
}

... потому что даже * pds требует больше памяти, чем это абсолютно необходимо.

1 голос
/ 23 марта 2010

Похоже, вы действительно знаете, что делаете. Вы профилировали свое приложение и точно знаете, где используются циклы. Вы понимаете, что вызов конструктора для указателя подсчета ссылок стоит дорого, только если вы делаете это постоянно.

Единственное замечание, которое я могу вам дать: предположим, что внутри функции f (t * ptr), если вы вызываете другую функцию, которая использует общие указатели, а вы делаете другие (ptr), а другие делают общий указатель необработанного указателя , Когда счетчик ссылок второго общего указателя достигнет 0, вы фактически удалили свой объект ... даже если вы этого не хотели. Вы сказали, что часто используете указатели подсчета ссылок, поэтому вам следует остерегаться подобных случаев.

EDIT: Вы можете сделать деструктор приватным и только другом класса общего указателя, так что деструктор может быть вызван только общим указателем, тогда вы в безопасности. Не запрещает множественное удаление из общих указателей. Согласно комментарию от Мат.

...