Стоимость передачи shared_ptr

Я использую станд.:: 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 не должен использоваться для функций, которые вызываются очень часто.

Любой вход ценился бы. Спасибо за чтение.

- Артем

Обновление: После изменения горстки функций для принятия 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&)

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

57
задан Artem Sokolov 23 March 2010 в 23:41
поделиться

5 ответов

Всегда передавайте свой 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 & , если аргумент не должен быть нулевым.
  • Использование const shared_ptr & безопаснее, чем const T * , когда производительность является проблемой.
57
ответ дан 24 November 2019 в 19:43
поделиться

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

4
ответ дан 24 November 2019 в 19:43
поделиться

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

Подумайте, что делает 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 требует больше памяти, чем это абсолютно необходимо.

3
ответ дан 24 November 2019 в 19:43
поделиться

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

10
ответ дан 24 November 2019 в 19:43
поделиться

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

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

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

1
ответ дан 24 November 2019 в 19:43
поделиться
Другие вопросы по тегам:

Похожие вопросы: