вторник, 1 января 2030 г.

О блоге

Более двадцати лет я занимался разработкой ПО, в основном как программист и тим-лид, а в последние пару лет как руководитель департамента разработки и внедрения ПО в компании Интервэйл (подробнее на LinkedIn). Поэтому в моем блоге много заметок о работе, в частности о программировании и компьютерах, а так же об управлении.

Так же я пишу о жизни вообще и о нескольких своих увлечениях: о фотографии (включая публикацию своих фотографий, некоторые есть и на ZeissImages), о спорте, особенно о дартсе, и, совсем коротко, о кино.

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

[prog.sobjectizer] Цели, которые будут поставлены перед механизмом overload control.

Понимаю, что то, что я пишу в блоге про SObjectizer, интересует малую долю моих читателей. Однако, в данном случае нужно задействовать блог в качестве канала для привлечения желающих пообсуждать очень интересную и непростую тему. А именно: тему защиты агентной системы от перегрузки (overload control). Поэтому прошу прощения у тех, кому не интересно, всех же остальных приглашаю под кат.

Тема очень непростая. Помнится, лет семь назад мы очень плотно ее обсуждали с Дмитрием Вьюковым, тогда применительно к SObjectizer-4. Но до чего-то путного не договорились. Теперь же у меня есть буквально неделя-две, чтобы включить какой-то базовый механизм overload control в релиз версии 5.4.0. А конь, как говорится, еще не валялся. Так что задача, может, и безнадежная, но зато увлекательная.

Дело в том, что если мы работаем в рамках компонентной системы, в которой преобладают синхронные механизмы взаимодействия между компонентами, то сами эти механизмы выступают естественными регуляторами нагрузки. Например, представим себе классический Unix-овый подход, когда стандартный поток вывода одной программы подается на стандартный поток входа другой программы:

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

[life] Актуальное противопоставление "Солдат морской пехоты / Ветеран"

Актуальность навеяна многим. В том числе и осознанием того, что произнесенная вслух фраза "Я же старый больной человек", уже содержит лишь долю шутки...

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

Источник: Созданные равными на AdMe.ru.

[life.work] Жизненно, блин...

Бернард Шоу:

Мир состоит из бездельников, которые хотят иметь деньги, не работая, и придурков, которые готовы работать, не богатея.

Источник: AdMe.ru

[prog.c++] Чего ждать от lock-free структур при активном использовании динамической памяти?

В последние несколько дней активно экспериментировал с lock-free и не lock-free очередями для передачи данных между нитями. Хотел выжать максимум возможного. Получил несколько обескураживающие результаты и сделал для себя соответствующие выводы. Полагаю, что для опытных в многопоточном программировании людей все нижесказанное будет очевидными и давно известными вещами. К сожалению, таковыми для меня они не оказались. Потому, что вижу, о том и пою, гуру от многопоточности этот текст могут не читать.

Итак, если вы активно используете в своем C++ приложении передачу динамически созданных объектов между нитями, да так, что одна нить создает объект, а вторая нить затем его разрушает, то вы не получите очень уж серьезного выигрыша от lock-free очередей (или других механизмов синхронизации передачи информации между потоками, построенных на низкоуровневых инструментах вроде атомиков).

Просто потому, что весь выигрыш, который дают вам lock-free структуры, будет без следа съеден стоимостью операции delete для объекта, аллоцированного в другом потоке. Т.е. если стоимость деаллокации объекта составляет 10мкс, то стоимость передачи указателя на него в другую нить в районе 1-2мкс -- это не о чем. Ваши потоки будут гораздо больше времени ждать на аллокации/деаллокации, чем на CAS-ах или даже тупых spinlock-ах.

Под катом полностью самодостаточный тест, который можно попробовать у себя. Он делает вот что:

Сначала создает N нитей (задается в командной строке или определяется автоматически). Эти нити в цикле делают две операции: создают несколько объектов demand_t и сохраняет указатели на них объекте to_fill. Затем ждут, пока появятся объекты в объекте to_clear. Как только объект to_clear оказывается заполненным, его содержимое (т.е. все объекты demand_t) удаляются.

Смысл в том, что нить может создавать объекты demand_t для удаления другой нитью. Тут все зависит от того, как именно заданы параметры to_fill и to_clear. Сначала тест запускается так, чтобы нить создавала demand_t в свой to_fill, а удаляла из чужого to_clear. Потом тест запускается так, чтобы и to_fill и to_clear были собственными объектами нити. В этом случае операции new/delete происходят на контексте "родной" нити и, потому, получаются гораздо эффективнее. Такое поведение теста контролируется параметром shift: значение 1 указывает, что to_fill будет "родным", а to_clear -- "чужим". Если же shift=0, то to_fill и to_clear -- это родные объекты нити.

У меня на 4-х аппаратных ядрах и четырех потоках под MSVC++2013 получаются следующие результаты:

$ _vc_12_0_x64/release/_experiments.combined_pass_between.exe 4
threads: 4
...
*** shift=1 ***
allocs: 4000000, total_time: 0.953s
price: 2.3825e-007s
throughtput: 4197271.773 allocs/s
assigns: 4000000, total_time: 0.65s
price: 1.625e-007s
throughtput: 6153846.154 assigns/s

*** shift=0 ***
allocs: 4000000, total_time: 0.168s
price: 4.2e-008s
throughtput: 23809523.81 allocs/s
assigns: 4000000, total_time: 0.015s
price: 3.75e-009s
throughtput: 266666666.7 assigns/s

Здесь при shift=1 пропускная способность для передачи динамически выделенных объектов между нитями получилась порядка 4.2M объектов в секунду (4_197_271). Тогда как при shift=0 нити смогли создавать и удалять объекты с суммарной пропускной способностью 24M объектов в секунду (23_809_523).

Отдельной строкой (помеченной как assigns) стоят результаты еще одной модификации этого теста: вместо реальной аллокации и деаллокации объектов demand_t между нитями передаются указатели на статические объекты. Т.е. вызовов new/delete нет, зато есть ожидание на атомиках. Имхо, такой вариант теста показывает максимальную скорость обмена значениями unsigned int между нитями. Т.е. в случае 4-х потоков, каждый из которых читает и пишет по два unsigned int для взаимодействия с другим потоком, суммарная пропускная способность передачи значений составила 6M в секунду. Для сравнения, когда эти unsigned int-ы просто изменяются одной и той же нитью, которая, к тому же не вызывает new/delete, "суммарная пропускная способность" составляет 267M (хотя в этом случае такой термин вряд ли применим вообще).

Цифры 4.2M (для allocs) и 6M (assigns) можно интерпретировать по-разному. При желании можно их удвоить, т.к. каждая нить выполняет операции чтения-ожидания-записи над двумя atomic-ами. С другой стороны, если нить пишет в очередь исходящие сообщения и принимает входящие, то как раз два atomic-а и потребуются. Поэтому я считаю именно так: 4.2M обменов сообщениями между нитями. Хотя, даже если цифры и удвоить, результаты получаются совсем не такие впечатляющие, как в случае с локальной работой каждой нити.

Под Linux и GCC 4.9.0 получается аналогичная картинка (хотя я пускаю тест под VirtualBox с эмуляцией двух процессорных ядер, т.ч. на реальном железе показатели могут отличаться в ту или иную сторону):

threads: 2
...
*** shift=1 ***
allocs: 2000000, total_time: 3.067s
price: 1.5335e-06s
throughtput: 652103.0323 allocs/s
assigns: 2000000, total_time: 2.941s
price: 1.4705e-06s
throughtput: 680040.8024 assigns/s

*** shift=0 ***
allocs: 2000000, total_time: 0.134s
price: 6.7e-08s
throughtput: 14925373.13 allocs/s
assigns: 2000000, total_time: 0.022s
price: 1.1e-08s
throughtput: 90909090.91 assigns/s

Какой практический вывод из всего этого напрашивается? Таковых несколько:

  • во-первых, использование динамической памяти желательно сводить к минимуму, если есть необходимость передавать мелкие объекты между сущностями, намного выгоднее это делать посредством копирования значений;
  • во-вторых, чем больше работы можно сделать на контексте одной нити, тем лучше. Задействовать многопоточность нужно в условиях, когда требуется действительно вытесняющая многозадачность и независимое выполнение каких-то операций (вроде синхронного ввода/вывода, обращения к внешним устройствам/службам, работа с СУБД и т.д.). Либо же в случае, если стоимость выполняемой на другой нити операции реально очень высока и выделение ее на отдельное аппаратное ядро принесет выигрыш (скажем, обработка больших объемов данных, которые можно раздробить на части и поручить обработку независимых частей разным ядрам);
  • в-третьих, при передаче информации между независимыми нитями нужно стараться как можно чаще копировать данные, и как можно реже передавать ответственность за освобождение динамической памяти другой нити.

Ну и, в продолжение практических выводов (кто про что, а вшивый про баню -- я про SObjectizer). В случае реализации модели акторов (агентов, активных объектов) передача сообщений между акторами посредством динамически-созданных объектов -- это губительно для масштабирования. По крайней мере в случае, если память освобождает не та нить, которая выделила память. Поэтому идеальный вариант очередей сообщений для акторов/агентов -- это очередь копий экземпляров сообщений, а не очередь указателей на динамически созданные экземпляры. (С другой стороны, тут еще нужно посмотреть, как оно будет, если у сообщения сразу несколько получателей, а размеры сообщений исчисляются десятками килобайт или же сообщения содержат в себе объекты с указателями внутри, как те же std::string-и или std::vector). Либо же, нужны какие-то механизмы очистки памяти (memory reclamation), чтобы заявка, сформированная нитью-отправителем, не уничтожалась нитью-получателем, а специальным образом помечалась. После чего нить-отправитель могла бы ее подчистить.

Тут, кстати говоря, на повестку дня вновь выходит вопрос о контроле за нагрузкой на акторов/агентов. Например, если можно указать, что для нормальной работы агенту нужно всего 300Kb, то для агента выделяется блок в 300Kb, внутри которого аллоцируется память под адресованные агенту сообщения. Получается такая циклическая арена, внутри которой хранятся сообщения агента и работа с которой происходит максимально быстро... Но это уже совсем другая история.

Под катом обещанный код теста. Красотой не блещет, но мне нужен был quick & dirty вариант для быстрых экспериментов, потому марафет не наводился. Тип demand_t имеет такой вид, чтобы быть более-менее похожим на описание заявки в SObjectizer-е.

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

[prog.c++] Досадный баг в MSVC++ 2013

Современный C++ позволяет писать довольно лаконично. Например, если есть вот такая структура с функциями-фабриками:

struct factories_t
{
   std::function< dispatcher_unique_ptr_t() > m_disp_factory;
   std::function< binder_unique_ptr_t() > m_bind_factory;
};

То можно написать вот такую функцию, которая создает экземпляры данной структуры:

factories_t
create_factories( const std::string & name )
{
   if"first" == name )
      return {
         []() { return create_first_dispatcher(); },
         []() { return create_first_binder(); }
      };

   return {
      []() { return create_second_dispatcher(); },
      []() { return create_second_binder(); }
   };
}

К сожалению, в MSVC++ 2013 подобный код работает только, если написанные в create_factories() лямбды не требуют захвата каких-либо переменных. Если же требуют, т.е. если написать лямбду вот так:

return {
   []() { return create_first_dispatcher(); },
   [name]() { return create_first_binder(); }
};

То при попытке обратиться к ней возникнет исключение с текстом "bad function call" (т.е., похоже, в этом случае лямбда не преобразуется корректно в объект std::function).

Между тем под GCC (4.8.3 и 4.9.0) нормально работают и вариант без захвата контекста, и вариант с захватом контекста.

Пичалька...

Под катом полный самодостаточный код теста для самостоятельной проверки.

[prog.c++] Небольшая демонстрация поведения разных диспетчеров в SObjectizer

В качестве демонстрации диспетчеров thread_pool и adv_thread_pool сделал сегодня небольшой пример, который имитирует некую длительную, блокирующую рабочую нить работу. В примере создается три агента:

Агент-менеджер, который отсылает N сообщений do_hardwork агенту-исполнителю, получает от исполнителя отчеты о проделанной работе (hardwork_done). Получая каждый экзепляр hardwork_done агент-менеджер отсылает сообщение check_hardwork третьему агенту -- агенту-контроллеру. В ответ контроллер должен прислать hardwork_checked. Когда на все N сообщений do_hardwork будут получены N сообщений hardwork_done и hardwork_checked, агент-менеджер отображает отчет о затраченном времени и завершает работу.

Агенты исполнитель и контроллер в своих обработчиках сообщений do_hardwork и check_hardwork для имитации занятости вычислительного ресурса просто засыпают на указанное количество миллисекунд. А после пробуждения отсылают менеджеру hardwork_done и hardwork_checked соответственно. Поскольку агенты засыпают на рабочих нитях диспетчера, то диспетчер не может их переиспользовать, пока агенты не проснуться. Т.е. выполняется такая дешевая имитация какой-то длительной работы (например, выполнение какого-то синхронного вызова, вроде обращения к БД или записи данных в сокет).

Пример позволяет выбрать, на каком диспетчере будут работать агенты. Можно указать либо one_thread, либо active_obj (каждый агент будет работать на своей собственной нити), либо thread_pool/adv_thread_pool. В случае thread_pool и adv_thread_pool агенты работают независимо друг от друга, в режиме individual FIFO. В случае active_obj диспетчера будет создано три рабочих нити (по количеству агентов). В случае thread_pool/adv_thread_pool будет создано столько рабочих нитей, сколько ядер определят C++ный рантайм (посредством std::thread::hardware_concurrency).

Соответственно, можно увидеть, как на суммарном времени работы теста сказывается возможность задействовать для обработки разное количество потоков. При использовании one_thread-диспетчера время получается строго линейным (т.е. количество запросов умноженное на величину тайм-аута и еще умноженное на два, т.к. тайм-ауты отсчитывают и агент-исполнитель, и агент-контроллер). У меня при параметрах 200 запросов и 20-мс тайм-ауте время составляет около 8 секунд.

Диспетчеры active_obj и thread_pool на 4-х ядерной машине дают время порядка 4 секунд. Т.е. за счет параллельной обработки запросов do_hardwork и check_hardwork получается просто результат умножения количества запросов на время выполнения одного запроса.

А вот adv_thread_pool на 4-х ядерной машине позволяет запустить 4-ре рабочих потока и распределить обработку всех do_hardwork и check_hardwork запросов между всеми рабочими потоками. Что сокращает время обработки теста до 2-х секунд.

Что хотелось бы отметить: сами агенты в тесте вообще не знают, на каких именно диспетчерах они будут работать. Ничего специализированного и заточенного под конкретный диспетчер в их коде нет. Разве что агенты исполнитель и контроллер указывают, что свои сообщения они обрабатывают в thread-safe методах. Что и позволяет получить эффект от распараллеливания на adv_thread_pool диспетчере.

Исходный код примера под катом. Он без комментариев в коде, но там настолько все тривиально (для меня, по крайней мере), что я не нашел, что и где прокомментировать. Но, если у кого-то будут вопросы или непонятки по коду, не стесняйтесь спросить, с удовольствием отвечу и поясню что к чему.