пятница, 18 июля 2014 г.

[prog] Кому-нибудь будет интересно читать про подводные камни в разработке SObjectizer?

Пока есть возможность, работаю над SObjectizer full-time (иногда даже over full-time). Почему я это делаю за свой собственный счет -- это тема отдельного разговора. Однако, сейчас я хочу спросить своих читателей о другом.

В процессе разработки, как всегда внезапно, обнаруживаются подводные камни, о которых изначально даже и не подозреваешь. Процесс поиска путей их преодоления временами бывает увлекательным, приводящим к неожиданным решениям. Интересно ли кому-нибудь будет читать о том, с какими проблемами приходится сталкиваться при разработке SObjectizer и в каком направлении (и с какими ограничениями) происходит поиск решений?

Лично я вижу смысл таких заметок в том, что останутся следы, объясняющие, почему SObjectizer устроен и работает именно так, а не иначе. Но вот будет ли это интересно еще кому-то кроме меня -- не знаю. Поэтому прошу оставить свое мнение в комментариях. Если желающих читать такие опусы наберется 15-20 человек, тогда попробую начать что-то вроде "путевых заметок". Если не наберется, не буду никого грузить своими личными проблемами :)

Upd. Проголосовало "За": 8

среда, 16 июля 2014 г.

[prog.c++] std::this_thread::get_id() -- очень дорогая штука под Windows и MSVS2013

Просто на удивление. Так что, если кому-то нужно очень часто вызывать std::this_thread::get_id(), то это может привести к серьезной просадке производительности.

Под катом исходник standalone benchmark-а, на котором я проводил замеры. Под Windows 8.1 и MSVS2013 Express у меня получается чуть меньше 2M вызовов get_id в секунду (компиляция через cl -EHsc -O2). Тогда как под тем же Windows 8.1 и Cygwin (GCC 4.8.3) получается уже чуть больше 10M в секунду (компиляция через g++ -std=c++11 -O3). А под VirtualBox-ом, ArchLinux и GCC 4.9.0 -- уже больше 400M! (компиляция такая же: g++ -std=c++11 -O3).

Причем это явно какая-то хитрая особенность реализации std::thread::id и std::this_thread::get_id() в MSVS. Т.к. если тупо пользоваться GetCurrentThreadId(), то этот же тест выдает более 500M в секунду при тех же самых параметрах компиляции.

вторник, 15 июля 2014 г.

[prog.c++] Еще про тонкости ACE (не забывать про #include в .cpp-файлах с main-ом)

Сегодня из-за собственной забывчивости убил несколько часов на разбирательство с крахом совершенно безобидного unit-теста. Сначала удалось выяснить, что падение происходит при попытке обращения к ACE-овскому Timer_Queue_Adapter-у. Потом оказалось, что к такому поведению привело изъятие #include <ace/Basic_Types.h> из одного из заголовочных файлов.

Ларчик открывался просто. В каждом cpp-файле, где определяется main(), необходимо делать #include <ace/OS.h> (или OS_main.h, или ACE.h -- главное, чтобы хотя бы один из ACE-овских файлов был заргужен). Это необходимо потому, что ACE делает свое определение функции main, в котором скрывается инициализация и деинициализация внутренностей ACE. А символ main после этих определений оказывается всего лишь #define-ом, при использовании которого пользователь определяет не реальный main, а всего лишь вспомогательную для ACE функцию ace_main_i().

Вот в моем unit-тесте не было #include <ace/OS.h>, но ранее все работало из-за того, что в других заголовочных файлах подгружались ACE-овские заголовки и main() для ACE определялась правильно. Когда же я проделал рефакторинг, выяснилось, что больше никакие ACE-овские заголовки в unit-тесте не загружаются, main оказывается настоящей main, внутренности ACE не инициализируются должным образом, отсюда и крах приложения.

В общем, актуальность отказа от ACE в SObjectizer в очередной раз появилась на повестке дня. Правда, сделать это будет не так уж и просто. На вскидку, без тщательного изучения кодовой базы, можно говорить про следующие вещи, в которых ACE нужно будет заменить на что-то еще:
  • логирование сообщений о фатальных ошибках в ядре so_5. Это не критичная функциональность. Можно либо тупо перейти на работу только с std::cerr, либо же определить интерфейс, реализацию которого можно будет назначить через so_environment_params_t (по умолчанию будет задействована реализация на основе std::cerr);
  • реализация timer_thread. Очень важная штука, без таймеров нет SObjectizer-а. Сейчас в so_5 интерфейс timer_thread реализуется через ACE_Thread_Timer_Queue_Adapter. Можно сделать свою реализацию на основе только стандартной библиотеки C++11 (посредством std::map для хранения заявок и std::condition_variable::wait_until для ожидания наступления очередной заявки или поступления запроса на создание новой заявки/удаления существующей). Реализация не кажется слишком уж сложной, но пока она не будет в должной мере протестирована в реальных проектах коэффициент моего спокойного сна будет совсем никакой ;)
  • в подпроекте so_log нужно будет много чего переписать, т.к. там сейчас определены собственные реализации Backend-ов для ACE_Logging. Вроде бы ничего сложного;
  • самым кардинальным образом нужно будет переделать so_5_transport, в котором вся работа с сокетами построена на ACE_Socket-ах и ACE_Reactor-ах. Очень нехилый кусок работы. Тут важно выбрать адекватную замену и понять, какие же выигрыши будут от такой замены. В качестве альтернатив ACE я бы здесь рассматривал libevent/libev, libuv и, может быть, Boost.Asio. На крайний случай можно и POCO. Если кто-то знает другие хорошие библиотеки для организации сетевого взаимодействия (с нормальной поддержкой Windows и Linux), прошу поделиться знаниями;
  • работа с DLL в so_sysconf. В принципе, это довольно простая для создания кросс-платформенности абстракция, так что можно будет сделать свою обертку над LoadLibrary и dlopen;
  • запуск приложения в режиме сервиса Windows или демона Unix, так же используется в so_sysconf. Такие вещи я сам вручную не делал, использовал функциональность ACE. На что заменять и насколько сложно сделать свою реализацию вручную не имею представления;
  • во многих примерах/тестах/приложениях используется функциональность ACE_Get_Opt для работы с аргументами командной строки. И хотя кода для работы с ACE_Get_Opt приходится писать много, результат того стоит, т.к. обеспечивается чуть ли не полная поддержка POSIX-овского стандарта на формат аргументов командной строки. На что менять не знаю. На ум сразу приходит Boost.ProgramOptions, но стоит ли только ради этого закладываться на зависимость от Boost-а -- не понятно.
В общем, работы нужно будет проделать прилично. Оправдано ли это -- не знаю, далеко не уверен. Если бы к нам были обращения о том, что SObjectizer -- штука хорошая, но использованию препятствует ACE, можно было бы проделать полный переход с ACE на что-то другое. Но таких обращений не было, поэтому приоритет у этой задачи самый маленький.

Похоже, что максимум, на который я смогу решиться в ближайшие несколько недель, это полный отказ от ACE в so_5, т.е. написание своей реализации timer_thread. Все остальное же будет отложено до лучших времен (если таковые вообще наступят).

понедельник, 14 июля 2014 г.

[management] Краткосрочный приход Рона Джонсона в JC Penney

Перед выходными попала на глаза заметка про несколько ошибок, допущенных Роном Джонсоном (Ron Johnson) на посту CEO компании JC Penney. Ранее я про эту историю ничего не знал, т.к. крупным американским бизнесом не интересовался. А история, как оказалось, поучительная.

Суть в том, что когда в 2011 у JC Penney наметились некоторые проблемы и появились новые акционеры, а у тогдашнего CEO Майка (Мирона) Уллмана (Myron Ullman) были нелады со здоровьем, на пост CEO был приглашен глава Apple Retail Рон Джонсон, под руководством которого фирменные магазины Apple показали мировые рекорды по прибыльности. JC Penney объявил о смене CEO летом 2011, а официальное назначение произошло осенью 2011.

В начале апреля 2013 Рон Джонсон был уволен. По итогам четвертого квартала 2012 года JC Penney зафиксировал падение выручки на $4.3млрд. и убыток в $1млрд (объемы продаж в магазинах упали на 25%-32%). В феврале 2013-го стоимость акций JC Penney снизилась на 46%.

Вот так за полтора года успешный управляющий из Apple выступил полным неудачником в JC Penney.

Новый логотип JC Penney представленный Джонсоном. Для компании это был уже третий логотип за три года.

В Интернете можно найти несколько статей, разбирающих допущенные Джонсоном ошибки. Как я понял, все дело в том, что человек, привыкший продавать понтовые штучки от Apple хипстерам, не смог разобраться, как же работает крупная розничная сеть по продаже обычных товаров, основными покупателями в которой являются далеко не хорошо обеспеченная молодежь, остро и шустро реагирующая на перемены моды, а обычные домохозяйки и немолодые люди, привыкшие покупать вещи в JC Penney десятилетиями (сама компания имела 109 летнюю историю).

Для меня гораздо интереснее несколько причин, которые хорошо поясняют, почему Джонсон не смог понять, зачем люди идут в магазины JC Penney и почему уже устоявшаяся схема работы торговой сети именно такая.

Ему это не нужно было. Он сам заявлял, что пришел в JC Penney не улучшать, а преобразовывать. Ну и начал преобразовывать. Поувольнял большое количество ТОП-менеджеров, заменив их людьми "со стороны", часть из которых привел с собой из Apple, а следом -- и кучу менеджеров среднего звена. После чего начал свои преобразования не имея ни возможности, ни, как говорят, желания обсуждать нововведения со старыми сотрудниками.

Мне очень понравился абзац из одной из статей, описывающий, что происходило в JC Penney с приходом нового, революционного руководства:
With respect to the J.C. Penney culture, it was treated with disdain, and was assumed to be the source of the problems. Johnson rapidly parachuted in executives and senior managers from outside of J.C. Penney, many of whom were from Apple. There was no effort made to integrate the newcomers into the culture. A large acrylic cube, essentially a dumpster, was placed in J.C. Penney headquarters, and employees were encouraged to dump all branded items from the ‘old J.C. Penney’ there and start afresh. Cultural issues quickly emerged. The new leaders coined the term DOPEs, which meant Dumb Old Penney Employees. Some incumbents responded by coining the term ‘Bad Apples’ for their new teammates.
Что в моем вольном переводе, но ручаясь за смысл каждой строки ;), может быть изложено как-то так:
Что до корпоративной культуры JC Penney, то к ней относились с презрением и она рассматривалась как часть проблемы. Джонсон быстро набрал со стороны новых ТОП-менеджеров, многие из которых были из Apple. Не было никаких усилий по интеграции новичков в корпоративную среду. В штабквартире JC Penney был установлен большой акриловый куб (по сути это была здоровенная мусорка) и сотрудникам было предложено бросать туда любые вещи, ассоциирующиеся со "старым JC Penney", чтобы можно было начать "с нуля". Вскоре появились связанные с несовпадением культур проблемы. Новые лидеры компании ввели в обиход термин DOPE, который означал "тупых старперов из Penney" (Dumb Old Penney Employees). На что некоторые старые сотрудники ответили ярлыком "Яблочники херовы" (Bad Apples) для своих новых коллег.
Знакомая, блин, история. Ну и результат так же знакомый. Печально еще и то, что ТОПы, перемещаясь из компании в компанию, таскают за собой толпы своих людей (что, в общем-то понятно: на старом месте новому начальству они не нужны, а на новом месте потребуются свои, преданные и проверенные кадры). Поэтому после прихода новой ретивой команды ТОПов, лихо берущихся за перестройку того, что работает не так, как они привыкли, сложно ожидать чего-нибудь другого.

Ссылки по теме:
"There's Only One Steve" - A Story of Innovation Failure и "4 Mistakes that Cost $4 Billion" - A Story of Innovation Failure -- две статьи из одной серии на LinkedIn (со временем должна появится третья).
The 5 Big Mistakes That Led to Ron Johnson’s Ouster at JC Penney.
How Ex-CEO Ron Johnson Made JCPenney Even Worse -- небольшое краткое перечисление основных моментов "большого пути" Джонсона в JC Penney.
Inside J.C. Penney's "Cleanse" -- публично доступный фрагмент большой статьи о том, что и как происходило в истории JC Penney и Рона Джонсона. Полный текст статьи доступен подписчикам Fortune.

Upd. Еще одна статья про эту историю, точнее, про последствия. В ней, в частности, сказано, что по следам произошедшего с JC Penney такая контора, как Credit Suisse, провела исследование судьбы 17 ритейлеров, потерявших от 15% до 25% выручки в течении года (брался период с 2000 по 2011 годы). Только четыре ритейлера смогли выкарабкаться. Остальные либо были куплены/поглощены, либо объявлены банкротами.

воскресенье, 13 июля 2014 г.

[prog.c++] И я присоединился к полку писателей собственных примитивов синхронизации

Не от хорошей жизни, чесслово! :) Просто на некоторых операциях ну уж очень не хочется использовать тяжелые примитивы синхронизации, вроде std::mutex или ACE_RW_Thread_Mutex. А хорошие альтернативные реализации, например, как в библиотеке CDS, потребуют подтягивания дополнительных зависимостей. Вот и ловишь себя на мысли, что какой-нибудь spinlock -- это же совсем маленькая штука, всего на несколько строк (см., например, здесь), так почему бы и не написать... :)

Под катом две реализации RW-lock-а, т.е. примитива синхронизации, который позволяет обращаться к ресурсу либо нескольким "читателям", либо всего одному "писателю". Реализации тривиальные, не обеспечивающие "честности". Кроме того, если захватившая замок нить ломается, то замок остается в занятом состоянии. Однако, для моих нужд этого хватает.

Первая реализация, под названием rw_spinlock -- это примитивная реализация на двух atomic-ах, до которой додумался самостоятельно. Она компактная и понятная. Хотя и не очень эффективная.

Вторая реализация, под названием reader_priority_rw_spinlock -- это переписанный мной вариант класса RWSpinRPrefT из библиотеки CDS. Только моя реализация использует исключительно std-шные штуки и битовые операции, тогда как CDS-ный класс RWSpinRPrefT применяет собственные определения (которые могут, как я понимаю, мапиться на std- или boost-, или еще на что-нибудь), а так же битовые поля и union-ы. В reader_priority_rw_spinlock кода больше, чем в rw_spinlock. Но, надеюсь, ничего я не напутал. Данная реализация местами чуть побыстрее rw_spinlock.

На таком низком уровне я и до перехода в менеджеры не программировал, поэтому сейчас вообще тяжко. Вкуривал значение констант std::memory_order_* долго и упорно, но не уверен, что все воспринял. Кто интересуется этой темой, вот хорошая статья на эту тему на русском языке (как мне показалось ее написал автор библиотеки CDS). Поэтому если кто-то сведующий заметит проблемы в моем коде, прошу сразу же ткнуть меня мордой лица в соответствующие кучи :)

Заодно интересно пообщаться с кем-нибудь вот на какую тему. Допустим, у нас есть несколько рабочих потоков (нитей) и очередей сообщений для обмена сообщениями между потоками, каждому потоку принадлежит своя очередь. Каждый поток зависает на ожидании очередного сообщения, просыпается при его появлении, обрабатывает сообщение и, если очередь оказывается пустой, засыпает вновь.

Простая реализация такой очереди на C++11 требует одного std::mutex-а и одного std::condition_variable.

Однако у такой простой реализации есть проблема: если две нити начнут использовать такую очередь для ping-pong-а единичных сообщений, то производительность будет крайне низкой. Речь будет идти всего о сотнях тысяч сообщений в секунду. На мой взгляд это из-за того, что когда первая нить завершает обработку своего сообщения и ничего больше не обнаруживает, то засыпает на condition_variable слишком надолго. Попутно для работы с condition_variable несколько раз захватывается и освобождается std::mutex, что есть слишком долго и дорого.

Если заставить нити блокировать очереди сообщений посредством spinlock-ов и обходиться без засыпания на condition_variable, то производительность значительно увеличивается.

Но, со spinlock-ами плохо то, что они оправданы, если между нитями действительно идет постоянный интенсивный обмен сообщениями. Если же этот обмен имеет спорадический характер (т.е. временами пусто, временами густо), то spinlock-и будут зазря растрачивать процессорное время в ожидании очередного всплеска обмена сообщениями.

Поэтому на ум приходит тривиальная штука: при ожидании следующего сообщения на пустой очереди сначала провести несколько итераций на spinlock-е, а затем заснуть на std::mutex+condition_variable. Тогда получится, что пока сообщения идут плотным потоком, мы не трогаем "тяжелые" объекты синхронизации. Но, как-то только возникает какая-то заметная пауза, переходим в честный режим ожидания, позволяя ОС выделить освобожденные нами ресурсы другим нитям.

Кто-нибудь сталкивался с таким гибридным подходом? Если да, то можно ли где-то посмотреть (в смысле есть ли OpenSource-проекты с использованием такой схемы, чтобы глянуть на готовые реализации)? Какие были грабли? Оправдан ли такой подход (если нет, то почему)?