Pull to refresh

Производительность shared_ptr и C++11: почему я не верю библиотекам

Reading time 5 min
Views 25K
Здравствуйте!

Оптимизировал я однажды критический участок кода, и был там boost::shared_ptr… И понял я: не верю я библиотекам, хоть и пишут их дядьки умные.

Детали под катом.

Так вот, оптимизировал я код, и был там такой участок:
auto pRes = boost::static_pointer_cast< TBase >( boost::allocate_shared< TDerived >( TAllocator() ) );
// ... Doing something with pRes
return std::move( pRes )

Оптимизация подходила к концу, поэтому был собран релиз, а я решил посмотреть в дизассемблере чего же мне накомпилировала там любимая студия, ожидая увидеть что-то красивое и быстрое. Вот только увиденное повергло меня в шок:
; ---------------------------------------------------------------------------------------------
; Line 76: auto pRes = boost::static_pointer_cast< CBase >( boost::make_shared< CDerived >() );
 
  ; ... ничего интересного - готовят параметры
  call boost::make_shared<CDerived> (0D211D0h)  
  ; ... опять ничего интересного - готовят параметры
  call boost::static_pointer_cast<CBase,CDerived> (0D212F0h)
  ; ... снова ничего интересного - прием результата вызова
 
  ; похоже на проверку if( pRes ), на деле не важно. Важно что je НЕ ВЫПОЛНЯЕТСЯ
  test eax, eax
  je `anonymous namespace`::f+7Ah (0D210CAh) ; -> никуда не прыгаем, у нас pRes != 0
  ; ... ничего интересного
 
  ; Epic fail #1 - Interlocked Cmp Exchange
  ; Этот блок фактически выполняет удаление временного shared_ptr, созданного в результате
  ; вызова make_shared: тут уменьшают счетчик ссылок а потом делают условный jump,
  ; переход выполняется, если счетчик ссылок не ноль (что, очевидно, наш вариант,
  ; т.к. мы ведь создаем указатель).
  lock xadd dword ptr [eax],ecx  
  jne `anonymous namespace`::f+7Ah (0D210CAh) ; -> прыгаем на следующую строку в с++ коде
 
  ; ... тут еще есть потенциальное удаление указателя, но это dead code
 
; ---------------------------------------------------------------------------------------------
; Line 78: return std::move( pRes );
 
  ; Ассемблером, я, наверное, утомил.
  ; В этом блоке сначала вызывается Epic Fail #2 - Interlocked Increment, т.к. мы копируем
  ; pRes, чтобы вернуть значение. Затем Epic Fail #3 - Interlocked Cmp Exchange как результат
  ; удаления указателя pRes (освобождение памяти, естественно, не происходит)

Добавлю, что я умолчал, про еще 3 interlocked инструкции внутри вызовов make_shared и static_pointer_cast… Посмотрел я на это и стало мне плохеть на глазах. Это что же получается? Я тут специально move конструкторы вызываю, а они мне счетчик ссылок туда-сюда крутят?

* Лирическое отступление: чем это так плохо.
Я думаю все знают, что штуковина под названием умный указатель shared_ptr имеет внутри себя указатель на количество shared pointer-ов, ссылающихся на один и тот же хранимый объект. Когда мы копируем shared_ptr это самое количество увеличивается, а когда разрушаем — уменьшается. Во время разрушаения последнего shared pointer-а, количество ссылок становится ноль и вместе с ним удаляется и хранимый объект. Так вот, чтобы это все нормально работало в многопоточной среде, изменять количество ссылок нужно атомарными операциями, теми самыми, с ассемблерным префиксом lock: этот префикс гарантирует, что процессор точно-точно сделает все как надо, и никакие кеши не будут мешать нам жить. Префикс хороший, вот только медленный, очень медленный. Он замедляет команду приблизительно на 2 порядка, т.к. требует сброса кеш линии, а значит использовать его нужно как можно реже.

* Лирическое отступление 2: как так получилось и почему никаких атомарных инструкций быть не должно.
С++11 дал нам очень вкусную штуку, под названием move семантика. Теперь можно определить «перемещающие» конструкторы, которые перемещают данные из одного объекта в другой, вместо создания их копии. Такой конструктор, например, перемещает указатель на внутренний строковый буфер из одной std::string в другую, позволяя переместить строку из одного объекта в другой, не выделяя заново память. Точно также можно (и нужно!) перемещать счетчик ссылок из одного shared_ptr в другой. Действительно, в таком случае нам не нужно никаких атомарных операций, ведь мы не изменяем количество указателей. Мы всего лишь «переносим» все внутренние данные из одного в другой (при этом тот указатель из которого мы данные забрали больше уже никуда не указывает).

Так как же оно так получилось… Вероятно, недосмотрели. Хотел я написать в буст слезное письмо, уже даже начал это делать… Но тут нашел то, что сразило меня окончательно. Во время создания boost::shared_ptr функция get_deleter вызывает сравнение типов через typeid (о Боги!). Не знаю, как там у них, а мой компилятор делает это через strcmp (грустно, не правда ли?).

Тогда решил я измерить скорость стандартной библиотеки в сравнении с бустом. 2 раза! boost::make_shared медленнее std::make_shared в 2 раза! Почему, спросите Вы? Все просто, буст выделяет память под 2 объекта — счетчик ссылок и собственно хранимый объект. А вот стандартная библиотека — только под один, это объект содержит и то и другое. А выделение памяти — оно мееедленное. Устный плюс ушел в майкрософт, еще один попал туда же за то, что в стандартной библиотеки умные указатели работают как надо — move конструктор не делает никаких атомарных операций. Создание указателя проходит в lock free режиме… Ну, почти. static_pointer_cast все-таки не осилили они: он копирует указатель не смотря на то, что мог бы и переместить. Эта проблема решилась «допиливанием» библиотеки. не переносимым на другую платформу допиливанием, но зато соответствующим стандарту, можно скачать его здесь: pastebin.com/XZaE2cnW — работает в MSVC2010.

P.S.

Итак наш сегодняшний победитель — std от MSVC2010: имеет в сумме один плюсик
А вот бусту не повезло: -1

Ну а я прощаюсь, надеюсь, хоть кому-то эта информация была полезна. Используйте std::shared_ptr, выделяете память через make/allocate shared и будьте счастливы :)
Tags:
Hubs:
+60
Comments 49
Comments Comments 49

Articles