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

О блоге

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

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

понедельник, 31 декабря 2029 г.

[life.photo] Характерный портрет: вы и ваш мир моими глазами. Безвозмездно :)

Вы художник? Бармен или музыкант? Или, может быть, коллекционер? Плотник или столяр? Кузнец или слесарь? Владеете маленьким магазинчиком или управляете большим производством? Реставрируете старинные часы или просто починяете примус? Всю жизнь занимаетесь своим любимым делом и хотели бы иметь фото на память?

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

вторник, 2 декабря 2025 г.

[prog.c++] Интересно, а какой код понятнее?

В современном C++ одни и те же вещи можно сделать по разному.

Например, у нас есть список типов, для каждого из которых нужно сделать какое-то действие. Что-то вроде for_each-а, но для списка типов.

Можно сделать это вот так:

template<typename... Types>
void for_each_type_via_lambda(int arg) {
    const auto action = [arg]<typename T>() {
        std::cout << typeid(T).name() << " - " << arg << std::endl;
    };
    (action.template operator()<Types>(), ...);
}

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

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

template<typename T>
void do_something(int arg) {
    std::cout << typeid(T).name() << " - " << arg << std::endl;
}

template<typename... Types>
void for_each_type(int arg) {
    (do_something<Types>(arg), ...);
}

Результат будет один и тот же. Но вот понятность двух этих вариантов лично для меня совершенно разная.

Интересно, а какой из вариантов более понятен для вас?

PS. Этот пример на wandbox "для поиграться".

понедельник, 1 декабря 2025 г.

[life.cinema] Очередной кинообзор (2025/11)

Традиционный отчет о просмотренных фильмах. Традиционно в начале каждого из списков то, что понравилось больше, а в конце то, на что можно не тратить свое время. Впрочем, в этот раз из просмотренных сериалов не понравилось вообще ничего, так что там весь список можно смело обойти стороной.

Фильмы

Хищник: Планета смерти (Predator: Badlands, 2025). Посмотрел с удовольствием, мне понравилось. Наверное, это лучшее, что сняли после второй части "Хищника".

Кто она? (The Artifice Girl, 2022). Мне понравилось. Но не-айтишникам следить за происходящим на экране может быть сложновато.

Операция "Наполеон" (Napóleonsskjölin, 2023). Это что-то вроде современного Индианы Джонса на минималках, бюджет там явно был мизерный. Но по итогу на удивление неплохо и вполне себе смотрибельно.

Геля (2025). Вполне смотрибельно. Не всё понял по сюжету, но фильм смешной.

Франкенштейн (Frankenstein, 2025). Отлично снятая сказка. Если такой жанр нравится, то смело можно смотреть.

Призрак на поле боя (Un fantasma en la batalla, 2025). По сюжету очень перекликается с испанским же Агент под прикрытием. Но "Призрак..." чуть более суровый и мрачный, как мне показалось. Не шедевр, но смотреть можно.

Святая ночь. Охотники на демонов (Georukhan bam: demon heonteoseu, 2025). Простенько, бюджетненько, но вполне себе смотрибельно. Однако, этот фильм, скорее всего, для очень узкой аудитории: для тех кому нравится корейское кино и, одновременно, кино про демонов и экзорцизм.

Разрушитель миров (Worldbreaker, 2025). В принципе, не самое плохое фэнтези для подростков. Но мне совершенно не хватило экшОна, а без экшОна повествование получилось пресным и затянутым.

Туман (2023). Снято красиво, да. Но это единственное, что есть хорошего в фильме. Сама рассказанная история выглядит каким-то бредом.

Укрытие номер один (Safe House, 2025). Очень бюджетно и очень тупо. Лучше пройти мимо этого фильма.

Сериалы

Константинополь (первый сезон, 2025). Разочарован. Ждал чего-то вроде современной версии фильма "Бег", но увидел что-то невнятное про борьбу вымышленных ОПГ в вымышленном псевдоисторическом антураже. Жаль потраченного времени.

Лихие (оба сезона, 2024-2025). Развязка просто говно говна. Поэтому не рекомендую тратить на это кино свое время.

Ночной экспресс (Nightsleeper, первый сезон, 2024). Купился на высокий рейтинг на Кинопоиске. Редкий маразм. Жаль потраченного времени.

пятница, 28 ноября 2025 г.

[prog.c++] История с потреблением памяти из-за фрагментации хипа в mimalloc-е

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

Что, собственно, до некоторых пор и происходило: по показаниям htop было видно, как сперва потребление памяти растет, затем резко падает.

Но в какой-то момент поведение изменилось: потребление памяти росло во время первичной обработки, потом уменьшалось, но уменьшалось не настолько, насколько ожидалось. Грубо говоря, раньше в пике съедали 1Gb, затем падали до 400Mb. Теперь же в пике съедаем 1Gb, но затем падаем всего до 600Mb.

Естественно, возникло ощущение, что где-то есть утечка. Но никакой утечки не нашли. Зато нашли явные следы фрагментации хипа.

Сразу извинения: я не уверен на 100%, что термин "фрагментация" здесь является точным. Но лучшего, увы, не придумал. Поэтому прошу гнилыми помидорами не бросаться.

В проекте в качестве дефолтного аллокатора используется mimalloc. Хвала его создателям, в API mimalloc-а есть средства для интроспекции содержимого хипа, что и позволило обнаружить эффект фрагментации.

Нужно сказать, что mimalloc спроектирован так, чтобы максимально избавиться от вероятности фрагментации (насколько это в принципе возможно для языков без compacting GC): он использует арены (areas) под объекты фиксированного размера. Так, объекты размером в 16 байт создаются в своей арене, объекты размером в 32 байта -- в своей, размером 48 байт -- в своей и т.д. Тем самым mimalloc частично избавляется от эффекта "дырок в сыре" -- когда суммарно свободной памяти много, но вся она рассредоточена в мелких фрагментах, чередующихся с занятыми фрагментами.

Выяснилось, что под контролем mimalloc-а оказываются буквально сотни арен для объектов размером 16 и 48 байта, на которых живых всего пара-тройка объектов, а все остальное пространство пустое. Но т.к. на этих аренах есть живые объекты, то mimalloc не может отдать выделенную под арены память обратно ОС. Поэтому ОС показывает, что приложение держит больше памяти, чем мы рассчитывали.


Почему же так произошло?

Первичная обработка данных при старте создает в динамической памяти множество объектов. Среди них огромный процент объектов как раз имеют размер 16 и 48 байт. Под них mimalloc динамически выделяет все новые и новые арены, запрашивая память у ОС.

При завершении обработки эти вспомогательные объекту удаляются. И ранее их удаление приводило к очистке тех арен mimalloc-а, в которых объекты размещались. По мере освобождения арен mimalloc возвращал память ОС. И по показаниям htop было видно, что потребление памяти приложением снижается.

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

Происходило это потому, что в процессе обработки начали создаваться не только "старые" объекты размером 16 и 48 байт, но и "новые" -- внутри этого самого нового контейнера. Только вот содержимое нового контейнера должно было остаться до конца работы программы, поэтому эти "новые" объекты не удалялись. В отличии от "старых" объектов, нужных только на время первичной обработки.

Получалось, что сплошным потоком создаются объекты размером, например, 48 байт. Львиная их часть относится к "старым" объектам, но иногда создаются и "новые". Пропорция приблизительно 500 "старых" объектов к одному "новому". Т.е. создали 500 "старых" объектов, затем один "новый", затем еще 500 "старых", затем еще один "новый" и т.д.

Физически это выглядит так, что на очередной арене mimalloc-а подряд размещается 500 "старых" объектов, за ними один "новый", затем еще 500 "старых", следом еще один "новый", затем арена заканчивается, начинается новая, на которой сперва идет 500 "старых" объектов, затем один "новый" и т.д.

В итоге "старые" и "новые" объекты короткое время живут вместе внутри одних и тех же арен mimalloc-а. И когда после завершения первичной обработки "старые" объекты удаляются, то "новые" остаются. Именно они и являются той парой-тройкой все еще живых объектов внутри практически пустых арен.

В результате вроде как и утечек нет, все объекты корректно удалились. Но и сотни мегабайт неиспользуемой приложением памяти не были возвращены ОС.


К счастью выяснилось, что в реализации моего нового контейнера есть ошибка -- это самое дерево внутри содержало раз в 10 больше узлов, чем требуется. Тупо ошибся с расчетом размера одного узла дерева, поэтому узлы создавались слишком маленькими, а чтобы компенсировать уменьшившийся размер пришлось создавать больше узлов.

Отсюда и возникала пропорция одного "нового" объекта (тот самый узел дерева) к пятистам "старым" объектам.

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


Для меня главный практический вывод -- это то, насколько языки с GC (особенно с продвинутыми compacting GC) могут облегчить жизнь программиста. Да, там возникают другие проблемы. Но в данном случае compacting GC был бы очень в тему, на мой взгляд.

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

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

Если бы события развивались бы по такому худшему сценарию нам бы пришлось пересматривать политику создания "старых" объектов, которые нужны только на время первичной обработки данных.

понедельник, 24 ноября 2025 г.

[prog.c++] Не нужно делать собственные специализации std::hash<std::pair<T,U>>

Некоторое время назад я сам наткнулся на грабли при использовании библиотеки Folly. Мне потребовался std::hash для std::pair<int, some_my_type>, при этом std::hash для some_my_type у меня уже был. Поэтому я сделал специализацию std::hash для std::pair<int, some_my_type>, в которой использовал std::hash<some_my_type>. Собрал код, стал проверять и выяснил, что на некоторых тестовых сценариях происходит что-то странное. Начал разбираться и внезапно обнаружил, что мой std::hash для std::pair<int, some_my_type> не вызывается вообще.

Дело оказалось в том, что разработчики Folly подложили свинью своим пользователям и определили специализацию std::hash<std::pair<T, U>>. Чего делать по стандарту нельзя (за цитату из стандарта большое спасибо Константину Шарону):

• [namespace.std] (C++23 draft, §16.4.2.2/1):
The behavior of a C++ program is undefined if it adds declarations or definitions to namespace std or to a namespace within namespace std, unless otherwise specified.
• The only allowed exception is that you may specialize certain templates in std for user-defined types.

[unord.hash] (C++23 draft, §24.5.4.2/1): For all object types Key for which there exists a specialization std::hash, the program may define additional specializations of std::hash for user-defined types.

Тут особый упор нужно сделать на "for user-defined types", тогда как std::pair<T, U> -- это не user-defined type. Вот std::pair<int, some_my_type> -- это уже user-defined type и для него специализацию std::hash делать можно.

Об этом несколько месяцев назад я писал в LinkedIn. Но недавно на Reddit-е обнаружил ссылку на статью Simplify hashing in C++ в которой, ни много, ни мало, предлагается делать тоже самое, т.е. определять специализации std::hash для std::pair<T, U>.

Поскольку такое заблуждение существует, то приходится заострять на этом внимание. Поэтому, пожалуйста, не делайте собственные специализации std::hash<std::pair<T, U>>.

А если вы сомневаетесь в правильности того, о чем я говорю, то представьте себе ситуацию:

  • вы разрабатываете программу и определили какой-то собственный тип your_type;
  • вы захотели использовать your_type в качестве ключа в unordered_map и сделали для your_type специализацию std::hash;
  • затем вы захотели сэкономить себе время и силы и сделали специализацию std::hash для std::pair<T, U>. Теперь у вас автоматически поддерживается хеширование и для std::pair<int, your_type>, и для std::pair<your_type, int>, и прочих сочетаний your_type с другими типами, поддерживающими кэширование;
  • где-то в программе вы используете std::unordered_set<std::pair<int, your_type>>
  • все у вас работает нормально;
  • затем в один прекрасный момент вы подключаете в проект стороннюю библиотеку, в которую отдаете your_type, и которая использует std::pair<int, your_type> в качестве ключа в Folly::F14FastMap;
  • и тут оказывается, что у вас в проекте есть два определения для std::hash<std::pair<int, your_type>>

Поздравляю, у вас в коде Undefined Behaviour из-за нарушения one definition rule.

Реально хотите собственными руками подсаживать UB в код?


Разработчиков Folly понять можно: они делали Folly для своих собственных нужд и в их программах, разработанных под задачи Facebook-а, вряд ли могут встретиться другие специализации для std::hash. А то, что Folly применяют и за пределами Facebook-а, в том числе смешивая в одном проекте Folly с кучей других библиотек -- это не проблемы разработчиков Folly.

пятница, 21 ноября 2025 г.

[prog.c++] SObjectizer-5 пятнадцать лет!

Неумолимо быстро летит время и вот уже SObjectizer-5 исполняется пятнадцать лет.

Очень многое из того, я мог бы сказать по этому поводу уже сказано в 2020-ом году здесь. Если кто-то еще не читал тот пост, приуроченный к 10-летней годовщине, то настоятельно рекомендую сделать это, ибо все нижеследующее будет лишь дополнение к тому лонгриду.

Но прежде чем перейти к подробностям, слова искренней благодарности всем, кто тем или иным способом поддерживал наш проект. Вряд ли бы мы справились, если бы не ваш интерес и внимание. Спасибо от всей души!


Работа над SObjectizer в последние пять лет шла урывками.

В очередной раз повторюсь: непосредственно SObjectizer денег нам не приносит. Разве что создает некую репутацию и, как и RESTinio, приводит клиентов, которые увидев наши разработки предлагают нам тем или иным образом поучаствовать в решении их задач.

Т.е. зарабатывать на жизнь приходится не развивая SObjectizer за деньги внешних спонсоров, а трудясь на чужих проектах, зачастую никак с SObjectizer-ом (или RESTinio) не связанных. Что отнимает значительную часть времени и сил. А уже на том, что остается, делаются очередные версии SObjectizer-а.

Такова была ситуация в последние годы. Таковой она, судя по всему, и останется в будущем.

Поэтому бурного развития SObjectizer-а можно не ждать. Мы неторопливо и спокойно спускаемся с горы (если вы понимаете о чем я 😉)

Тем более что...


...SObjectizer уже давно вобрал в себя то, что было нужно именно нам. И если бы SObjectizer использовали только мы, то у SObjectizer-а случилось бы два-три корректирующих релиза, вряд ли больше.

То, что сейчас добавляется в SObjectizer, добавляется только потому, что это где-то кому-то потребовалось и этот кто-то нашел время и желание нам сообщить о своих хотелках.

При этом внутренняя сложность SObjectizer растет и эту сложность желательно держать под контролем. А один из самых действенных способов контроля -- это отказ от желания затащить в SO-5 все, что только возможно.

Поэтому период "добавляем фичу просто для того, чтобы была" закончился давным давно. Добавить что-то нужное и полезное -- это да, это мы с удовольствием. Но если никто ничего не просит, то значит имеющегося достаточно 😉


Одна из вещей, которой мы уделяли особое влияние, была совместимость между версиями SO-5. В течении почти пяти лет мы старались не ломать совместимость в рамках ветки 5.5. В середине 2019-го появилась ветка 5.6, в которой пришлось резать по живому.

А за 5.6 последовали ветки 5.7 (январь 2020-го) и 5.8 (июнь 2023-го). Но их я сам рассматриваю как последовательное развитие ветки 5.6. К сожалению, без ломающих изменений не обошлось. Однако, они не настолько кардинальные, как это было при переходе от 5.5 к 5.6. В основном изменения между 5.7 и 5.8 в касались очень специфических внутренних интерфейсов SObjectizer-а и в гораздо меньшей степени затрагивали обычный пользовательский код.

В этом же ключе собираемся двигаться и дальше. Ибо чем старше становлюсь, тем больше ценю возможность взять древний код и без лишних телодвижений заставить работать его с новыми версиями библиотек. Не могу гарантировать, что ветка 5.8 будет развиваться еще в течении 5 или 10 лет (хотя меня бы лично это очень бы порадовало), но мы будем к этому стремиться.


Есть одна важная фраза, написанная пять лет назад:

В общем, SObjectizer-5 поступательно развивается уже 10 лет, что лично меня приятно удивляет. Ведь запаса SObjectizer-4 хватило всего-то на 3-4 года. Тогда как SObjectizer-5 может эволюционировать еще несколько лет и насущной необходимости делать условный SObjectizer-6 на новых идеях пока не видно.

Можно констатировать, что время подтвердило эти слова: SObjectizer-5 все еще развивается и насущной необходимости делать условный SObjectizer-6 на новых идеях пока что не видно.

Это значит, что из опыта SObjectizer-4 были сделаны более чем правильные выводы.


Теперь же несколько слов об очень приятном факторе, который для меня до сих пор выглядит фантастическим.

В 2021-ом году SObjectizer-ом заинтересовался Марко Арена, лидер сообщества Italian C++ Community. И со временем он внес очень существенный вклад как в популяризацию SObjectizer-а, так и в его развитие. Так, сперва мой коллега, Коля Гродзицкий, сделал доклад о SObjectizer-е на виртуальном митапе. Затем Марко опубликовал замечательную серию статей о SObjectizer-е. А недавно Марко еще и выступил с собственным докладом о SObjectizer-е. Это первый из известных мне докладов о SO-5, который был сделан кем-то не из команды разработки SObjectizer-а.

Но кроме серьезной работы по продвижению SObjectizer-а "в массы" Марко задал множество хороших вопросов и высказал ряд интересных соображений, на основании которых затем в SObjectizer были добавлены новые фичи.

И это тот самый элемент везения, которого нам так не хватало и за который остается только поблагодарить судьбу. Марко, если ты вдруг читаешь этот пост, то огромное тебе спасибо! Смело могу сказать, что если бы не твое участие, SObjectizer не был бы сейчас так развит.


Ну и самый важный лично для меня аспект, а именно ответ на вопрос "почему я все еще занимаюсь SObjectizer-ом?"

Пять лет назад я уже дал исчерпывающий ответ на этот вопрос. Но этот ответ был актуален на 2020-й год: главной (но не единственной) причиной было желание сделать из SObjectizer-а полноценный продукт за который не стыдно.

Это было сделано. За SObjectizer-5 уже давно не стыдно. И за прошедшее время, надеюсь, наша разработка стала только лучше.

Но если эта цель была достигнута, то что движет мной теперь?

Вероятно, действуют вот эти причины:

  • просто потому, что могу;
  • потому, что это "мое", в самом широком смысле этого слова. Начиная от идей, заканчивая полной ответственностью при полной свободе в принятии собственных решений. Это то ощущение, которое никогда не получишь работая исполнителем на чужом проекте;
  • привычка. Это часть моей жизни в течении очень и очень долгого времени. Не самая плохая часть. И если у меня есть возможность продлить ее, то почему бы и нет?

Если кому-то интересны ответы на вопросы о том, что ждет проект дальше и насколько рискованно брать его в работу, то могу лишь сослаться на то, что писал пять лет назад:

Состояние SObjectizer-а и его перспективы

Брать или не брать такой SObjectizer в работу?

Там все уже было сказано. Сказать лучше не получится. Надеюсь только, что сам факт развития SObjectizer-5 в течении 15 лет что-то да говорит.


Чем сейчас можно помочь SObjectizer-у?

На мой взгляд, сейчас, как и 5 лет назад, SObjectizer больше всего нуждается в двух вещах:

  • распространении информации о нем. Если вы попробовали SObjectizer и он вам понравился или не понравился, то найдите, пожалуйста, время и возможность поделиться этим в Интернете. Поверьте, даже коротенький пост из одного предложения в Twitter-е оказывается огромным подспорьем в нашей работе;
  • обратной связи: если вам что-то не понравилось в SObjectizer или чего-то не хватает, сообщите, пожалуйста, нам об этом. Мы не сможем добавить в SObjectizer то, о чем не знаем.

Ну а теперь перечень фич и изменений, которыми оброс SObjectizer за прошедшие пять лет. В качестве краткого отчета о проделанной работе, так сказать 😀

Добавлена возможность брать под контроль создание рабочих нитей в штатных диспетчерах SObjectizer-а: тыц.

В класс agent_t добавлен метод so_deactivate_agent: тыц. Затем в довесок был добавлен метод so_drop_all_subscriptions_and_filters: тыц.

Для агента теперь можно назначить собственный MPSC-mbox в качестве direct-mbox-а: тыц.

Теперь delivery filters можно устанавливать даже на MPSC-mbox-ы: тыц.

Серьезно изменился интерфейс abstract_message_box_t. Добавилось понятие delivery_mode для того, чтобы можно было понять из какого контекста сообщения отсылаются (например, если сообщение идет с нити таймера, то в этом случае нельзя блокировать отправителя сообщения). При этом теперь mbox-ам не нужно заниматься контролем message limits. Тыц.

Добавлено понятие message_sink-а. Теперь в качестве подписчика для mbox-а может выступать не только агент, но и вообще произвольна сущность, реализующая интерфейс abstract_message_sink: тыц.

Добавлена возможность регистрировать в SOEnv именованные mbox-ы, которые были созданы пользователем, а не SObjectizer-ом: тыц.

Сделаны шаги в сторону лучшего обеспечения noexcept-гарантий со стороны SObjectizer-а при выполнении дерегистрации кооперации: изменен интерфейс event_queue_t, добавлены диспетчеры nef_one_thread, nef_thread_pool: тыц.

В класс agent_t добавлены методы so_5::agent_t::so_this_agent_disp_binder() и so_5::agent_t::so_this_coop_disp_binder() чтобы можно было узнать, с какими диспетчерами агент связан: тыц.

Появилась поддержка имен для агентов: тыц.

Расширен набор инструментов для юнит-тестирования агентов: тыц и тыц, и тыц.

Добавлена возможность выбросить все ждущие своей очереди заявки для агента при начале дерегистрации агента: тыц


Как-то все это получилось сухо и официально. Если жизнь предоставит возможность написать такой же пост в 2030-ом году, то постараюсь сделать аналогичный пост, посвященный 20-летию, более живым и эмоциональным 😉