Здравствуйте!
Оптимизировал я однажды критический участок кода, и был там boost::shared_ptr… И понял я: не верю я библиотекам, хоть и пишут их дядьки умные.
Так вот, оптимизировал я код, и был там такой участок:
auto pRes = boost::static_pointer_cast< TBase >( boost::allocate_shared< TDerived >( TAllocator() ) );
<font color="#666666">// ... 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/w1wSxbuf — работает в MSVC2010.
P.S.
Итак наш сегодняшний победитель — std от MSVC2010: имеет в сумме один плюсик
А вот бусту не повезло: -1
Ну а я прощаюсь, надеюсь, хоть кому-то эта информация была полезна. Используйте std::shared_ptr, выделяете память через make/allocate shared и будьте счастливы :)
Автор: Wyrd