Стоимость прохода по shared_ptr
Я использую 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&)
Я немного озадачен количеством вызовов деструкторов, которые меньше, чем количество вызовов конструктора копий, но в целом я очень доволен уменьшением связанного времени выполнения. Спасибо всем за их советы.
Ответы
Ответ 1
Всегда передавайте 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*
, когда производительность является проблемой.
Ответ 2
Вам нужно использовать shared_ptr только для передачи его функциям/объектам, которые сохраняют его для использования в будущем. Например, некоторый класс может поддерживать shared_ptr для использования в рабочем потоке. Для простых синхронных вызовов достаточно использовать простой указатель или ссылку. shared_ptr не должен полностью заменять обычные указатели.
Ответ 3
Если вы не используете make_shared, не могли бы вы дать это? Располагая счетчик ссылок и объект в той же области памяти, вы можете увидеть прирост производительности, связанный с когерентностью кеша. В любом случае стоит попробовать.
Ответ 4
Любое создание и уничтожение объектов, особенно создание и уничтожение избыточных объектов, следует избегать в критичных для производительности приложениях.
Рассмотрим, что делает 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 требует больше памяти, чем это абсолютно необходимо.
Ответ 5
Похоже, вы действительно знаете, что делаете. Вы профилировали свое приложение, и точно знаете, где используются циклы. Вы понимаете, что вызов конструктора в указатель подсчета ссылок стоит дорого, только если вы делаете это постоянно.
Единственными головами, которые я могу вам дать, является: предположим внутреннюю функцию f (t * ptr), если вы вызываете другую функцию, которая использует общие указатели, а вы делаете другую (ptr), а другая делает общий указатель необработанного указателя, Когда этот второй счетчик ссылок для общих указателей попадает в 0, вы фактически удалили свой объект... хотя вы этого не хотели. вы сказали, что вы часто ссылались на указатели ссылок, поэтому вам нужно следить за такими угловыми делами.
EDIT:
Вы можете сделать деструктор частным и только другом класса общего указателя, так что деструктор может быть вызван только общим указателем, тогда вы в безопасности. Не предотвращает множественные удаления из общих указателей. Согласно комментарию от Мата.