суббота, 20 сентября 2014 г.

[prog.c++] Тему с самодельными spinlock-ами закрываю. Не осилил

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

Поэтому эксперименты с реализацией spinlock-ов на чистом C++11 без привлечения внешних библиотек заканчиваю. Все, что удалось сделать, находиться вот в этой ветке Svn-репозитория. Лицензия у этого кода BSD-шная. Если кто-то хочет поковырять палочкой кучку -- you are welcome :) В принципе, я не против развить это во что-то нормальное, но только если этим будет заниматься кто-то с лучшим, чем у меня, пониманием предметной области. Меня хватит разве что на написание кода, дилетантские вопросы и провинциальную критику ;)

Что сейчас есть в spinlockspp? Есть самый простой спинлок, под названием trivial-lock, работающий на основе механизма test-test-and-set.

Есть очень тривиальная, но весьма медленная, реализация ticket-lock-а. В ней два атомарных счетчика. В то время как классическая реализация ticket-lock-а использует union, в котором лежат, скажем, одна 32-битовая переменная и структура из двух 16-битовых. Либо же просто одна 32-х битовая переменная, но для операции unlock задействуются только 16 бит из нее (т.е. применяется атомарный инкремент как будто к независимой 16-битовой переменной). Оказалось, что стандартными средствами С++11 этот фокус просто так не проделать. Поскольку std::atomic_fetch_add принимает в качестве аргумента не адрес ячейки памяти, а указатель на объект std::atomic. Тогда как C-шная функция atomic_fetch_add получает именно что адрес. И в C-шную fetch_add засунуть адрес части счетчика из ticket-lock-а несколько проще, чем в C++ (а в C++ прибегать к грязным хакам стремно, если честно). Чтобы задействовать C-шную fetch_add в C++ном коде, нужно, чтобы компилятор поддерживал и C++11, и C11. Но тот же Visual C++ 2013, например, не поддерживает C11. Поэтому ticket-lock такой примитивный.

Зато ticket-lock, в отличии от trivial-lock, является "честным".

Есть очень шустрая, но "нечестная" реализая RW-lock-а под названием dvyukov_rw_lock, разработанная Димой Вьюковым для проекта LLVM.

Отдельный вопрос -- это организация паузы в спинлоках, для случая, когда установить блокировку не удалось. Этот вопрос решается backoff-ами. Каждый спинлок реализуется шаблонным классом, параметризуемым конкретным типом backoff-а. В spinlockspp есть два платформенно независимых типа backoff-а. Это yield_backoff, который вызывает std::thread::yield(). И simple_inc_backoff_t, который использует простой цикл с инкрементом целочисленной переменной. Верхняя граница этого цикла постоянно увеличивается, так что при каждом новом ожидании пауза удлиняется.

Есть еще платформенно зависимый backoff под названием cpu_pause_backoff_t. Который использует инструкцию PAUSE для процессора. Правда, поскольку C++ не имеет стандартных средств выдачи таких инструкций, пришлось мудрить с #if/#else/#endif. С помощью онлайн компиляторов удалось получить вот такую штуку. Вроде под Visual C++/GCC/Clang/ICC должно работать. Понятное дело, что при переходе на другую платформу или другой компилятор, нужно будет делать новую реализацию SPINLOCKSPP_CPU_PAUSE.

Собственно, откуда пошли spinlock-и. От желания отказаться от использования ACE Framework и перейти только на стандартную библиотеку C++11. Но нужны были RW-мутексы, которых в C++11 нет. Зато есть средства работы с atomic-ами, поэтому появилась мысль наваять замену ACE_RW_Thread_Mutex на C++ных atomic-ах. В простой форме это было сделано и использовано.

А вот попытка сделать что-то более солидное закончилось пониманием, что одними только средствами C++11 обойтись не получится. А именно в этом и был интерес: оставаться только в рамках стандартной библиотеки и не адаптировать свой код под каждую платформу.

Но одним только C++11 не обойтись. Нужно глубоко погружаться в детали платформы и компилятора. Для этого нужно иметь соответствующую квалификацию. Либо желание и время ее приобрести. Чего в моем случае нет. Посему работы прекращаю.

Сухой остаток. Используйте спинлоки только в случае, когда точно уверены и имеете объективные доказательства профайлера о том, что производительности std::mutex-а явно недостаточно. При этом выполняемые под спинлоком действия должны быть очень короткими. И, кроме того, борющиеся за ресурсы нити должны быть распределены по разным ядрам процессора. Если же ядер меньше, чем потоков, то пользы от спинлоков может и не быть. Более того, можно еще и круто проиграть.

Ну а если спинлоки таки нужны, то лучше не делать их самому. А поискать что-нибудь готовое. Насколько много такого готового -- фиг знает. Для чистого C мне понравился код библиотеки Concurrency Kit. Есть ли что-то подобное для C++, да еще не под GPL лицензией -- это вопрос. Мне ничего на глаза не попалось. Разве что в составе каких-то больших библиотек/проектов (как с тем же rw_spinlock-ом от Димы Вьюкова).

Комментариев нет: