среда, 27 марта 2024 г.

[prog.thoughts] "Универсальный" против "специализированного" на примере из SObjectizer-а

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

В SObjectizer агенты подписываются на сообщения: если подписка есть, то сообщение до агента может дойти, а если подписки нет, то сообщение точно не дойдет. А раз есть подписки, то их нужно как-то хранить. Соответственно, возникает простой вопрос: "Как хранить?"

Фокус в том, что заранее неизвестно, сколько у агента будет подписок. Может быть две, может быть двадцать две, может быть две тысячи и двадцать две. А может и двести тысяч. Ну мало ли. Никто же не запрещает 😉

Как по мне, так это означает, что невыгодно в агенте хранить подписки одним и тем же способом вне зависимости от количества этих самых подписок. Ведь нет контейнеров, которые бы отлично работали бы при любом количестве элементов. Так, если у нас всего две подписки, то hash-таблица избыточна, как и бинарное дерево поиска (где каждый узел -- это отдельный объект в динамической памяти). А если у нас 100500 подписок, да они еще и активно создаются/уничтожаются, то непрерывный вектор здесь не вариант, т.к. вставки в середину (как и удаления из середины) дороги.

А раз так, значит напрашивается введение отдельного понятия, subscription_storage. Т.е. некого интерфейса, который будет скрывать детали хранения подписок. Соответственно, в агенте хранится ссылка на subscription_storage, а вся работа с подписками ведется через интерфейс subscription_storage.

Появление же этого интерфейса не может не сказаться на производительности. Ведь у нас появляется дополнительная косвенность. Плюс у самих реализаций subscription_storage могут быть какие-то дополнительные накладные расходы...

Вот и получается, что не зная конкретных сценариев использования приходится делать универсальное решение, которое, в лучшем случае, можно настроить под определенные условия.

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

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

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

ЗЫ. У SObjectizer-а давеча состоялся очередной релиз. Как раз была добавлена еще одна реализация subscription_storage. И таки дошли руки сделать описание этой штуки в Wiki-проекта.

среда, 20 марта 2024 г.

[prog.cmake] Попытался заглянуть в документацию по CMake. Пригорело. Нехило так пригорело :(

Я вот правда не понимаю, как этот кусок говна умудрился стать стандартом де-факто. И еще я не понимаю вот чего: нам, си-плюс-плюсникам, что, мало сложностей и несуразностей в самом C++? Мы так мало набили шишек на граблях C++, что радостно взялись скакать еще и по граблям CMake? Не, реально?

Вообще мне очень неприятно, что и меня, и разработчиков CMake называют программистами. А мне не хочется быть коллегой таких горе-разработчиков. Они уже упороты!

PS. Пожалуйста, не нужно спрашивать у меня чем заменить CMake. Уже почти 20 лет пользуюсь собственной системой сборки, написанной на Ruby. Когда-то даже прикладывал усилия чтобы продвинуть свой лисапед в массы, но не преуспел. На этом считаю свой долг по улучшению C++ной экосистемы полностью выплаченным.

вторник, 19 марта 2024 г.

[prog.c++.kill'em-all] Еще пример C++ного кода от которого у меня изрядно подгорает

Вот не нужно писать кроссплатформенный код вот так:

enum class status { not_started, started, shutting_down, stopped };

#if defined(PLATFORM_WINDOWS)
[[nodiscard]] const wchar_t * to_str(status st) noexcept {
  switch(st) {
    case status::not_started: return L"not_started";
    case status::started: return L"started";
    case status::shutting_down: return L"shutting_down";
    case status::stopped: return L"stopped";
  }
}
#else
[[nodiscard]] const char * to_str(status st) noexcept {
  switch(st) {
    case status::not_started: return "not_started";
    case status::started: return "started";
    case status::shutting_down: return "shutting_down";
    case status::stopped: return "stopped";
  }
}
#endif

вот не надо, пожалуйста.

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

Сделайте хотя бы так:

// Эта кухня должна жить в отдельном заголовочном файле.
#if defined(PLATFORM_WINDOWS)
  using platform_char_type = wchar_t;
  #define STRING_LITERAL(str) L##str
#else
  using platform_char_type = char;
  #define STRING_LITERAL(str) str
#endif

enum class status { not_started, started, shutting_down, stopped };

[[nodiscard]] const platform_char_type * to_str(status st) noexcept {
  switch(st) {
    case status::not_started: return STRING_LITERAL("not_started");
    case status::started: return STRING_LITERAL("started");
    case status::shutting_down: return STRING_LITERAL("shutting_down");
    case status::stopped: return STRING_LITERAL("stopped");
  }
}

четверг, 14 марта 2024 г.

[prog.flame] Самодокументирующися код против документированного, наглядно

Намедни в LinkedIn поиронизировал на счет "самодокументирующегося кода". В очередной раз 😎

В очередной раз с удивлением для себя обнаружил людей, скептически относящихся к необходимости комментировать код.

Подумал, что лучше одна иллюстрация лучше тысячи слов. Поэтому вот пример кода из реального проекта. Изменены только названия сущностей, все комментарии оставлены как есть. Плюс изъяты фрагменты, не относящиеся к самой иллюстрации.

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

Под катом оригинальный фрагмент с комментариями. Такими, какими они и были написаны при разработке. Но сперва этот же фрагмент, но вообще без комментариев. Именно так "самодокументирующися" код и выглядит, по моему (не)скромному опыту.

class DefaultThreadPoolScheduler final : public Scheduler
{
   ...

private:
   struct WorkerData
   {
      std::mutex _lock;
      std::condition_variable _wakeupCondition;

      Scheduler::TaskUniquePtr _taskToRun;

      bool _shutdownInitiated{ false };
   };

   using WorkerDataContainer =
      std::vector<std::reference_wrapper<WorkerData>>;

   std::latch _allWorkersStartedLatch;

   std::mutex _lock;

   TasksContainer _tasksQueue;

   ThreadPool _threadPool;

   WorkerDataContainer _availableWorkers;

   bool _shutdown{ false };
};

void DefaultThreadPoolScheduler::doWork() noexcept
{
   WorkerData thisWorkerData;

   {
      std::lock_guard schedulerLock{ _lock };
      _availableWorkers.push_back(std::ref(thisWorkerData));

      _allWorkersStartedLatch.count_down();
   }

   bool shutdownIntitiated{ false };
   while( !shutdownIntitiated )
   {
      std::unique_lock workerLock{ thisWorkerData._lock };

      if( TaskUniquePtr taskToRun = std::move(thisWorkerData._taskToRun); !taskToRun )
      {
         shutdownIntitiated = thisWorkerData._shutdownInitiated;
         if( !shutdownIntitiated )
         {
            thisWorkerData._wakeupCondition.wait(workerLock);

            shutdownIntitiated = thisWorkerData._shutdownInitiated;
         }
      }
      else
      {
         workerLock.unlock();
         taskToRun->run(Scheduler::RunCondition::Normal);
         completeTaskThenTryGetNext(std::move(taskToRun), thisWorkerData);
      }
   }
}

понедельник, 11 марта 2024 г.

[prog.c++] Первая попытка написать собственный концепт

В проекте для одного из клиентов уже используется C++20, так что есть возможность на практике прикоснуться к некоторым фичам двадцатого стандарта. Вроде ranges и concepts.

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

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

В общем, смысл в том, что есть интерфейс диспетчера задач Scheduler и есть интерфейс отдельной задачи Task:

class Scheduler
{
public:
   class Task
   {
   public:
      enum class Status { Normal, Deleted };

      virtual ~Task() = default;

      virtual void run(Status) = 0;
   };

   using TaskUniquePtr = std::unique_ptr<Task>;

   virtual void push(TaskUniquePtr task) = 0;
};

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

class FirstTask final : public Scheduler::Task
{
  ... // Какие-то внутренности.
public:
  ... // Какой-то конструктор.

  void run(Status status) { ... /* Какие-то действия */ }
};
...
scheduler.push(std::make_unique<FirstTask>(...));

предполагалось делать как-то так:

make_task(scheduler,
  [...](Scheduler::Task::Status status) {... /* Какие-то действия */ });

И вспомогательный шаблон функции make_task должен был обернуть лямбду в некий класс-наследник Task. Что-то вроде:

template<typename H>
class LamdaAsTask final : public Scheduler::Task
{
   H _handler;

public:
   explicit LamdaAsTask(H handler) : _handler{std::move(handler)} {}

   void run(Status condition) override
   {
      _handler(condition);
   };
};

template<typename H>
void make_task(
   Scheduler & scheduler,
   H && handler)
{
   scheduler.push(std::make_unique<LamdaAsTask<H>>(std::forward<H>(handler)));
}

Но ситуация была чуть сложнее. Нужно было использовать разные фабрики для Task. Т.е. в make_task передавались не только лямбда и ссылка на Scheduler, но еще и некая фабрика. Именно фабрика брала лямбду и возвращала указатель на новый Task. Т.е. шаблон make_task выглядел так:

template<typename Factory, typename H>
void make_task(
   Scheduler & scheduler,
   Factory && factory,
   H && handler)
{
   scheduler.push(factory(std::forward<H>(handler)));
}

И место для улучшения кода я видел в том, чтобы ограничить параметры шаблона make_task концептами.

Вырисовывалось два концепта.

Первый концепт описывает лямбду-обработчик. Ну типа ее можно вызвать с нужным параметром и она возвращает void.

template<typename H>
concept Handler_C = requires(H h)
{
   { h(Scheduler::Task::Status::Normal) } -> std::same_as<void>;
};

Второй концепт описывает фабрику Task-ов:

template<typename F, typename H>
concept TaskFactory_C = requires(F f)
{
   requires Handler_C<H>;

   { f(H{}) } -> std::convertible_to<Scheduler::TaskUniquePtr>;
};

Т.е. этот концепт зависит от параметра H, который описывает лямбду. Эта лямбда должна удовлетворять требованиям концепта Handler_C. А фабрика сама по себе должна поддерживать вызов operator() с передачей туда лямбды H и возвратом чего-то приводимого к TaskUniquePtr.

Благодаря таким концептам make_task приняла вид:

template<Handler_C H, TaskFactory_C<H> Factory>
void make_task(
   Scheduler & scheduler,
   Factory && factory,
   H && handler)
{
   scheduler.push(factory(std::forward<H>(handler)));
}

В общем, я получил то, что хотел.

Под катом полный код автономного примера, который всю эту кухню демонстрирует.

У меня же пока впечатления от концептов смешанные. Вероятно, вещь хорошая и, местами, полезная. Но вот ВАУ эффекта пока не случилось.

Вообще с C++ есть давняя история: сперва в нем появляется какая-то фича (в смысле описывается в стандарте языка), потом она становится доступна в каком-то из компиляторов, потом у тебя появляется возможность применять ее на практике, потом ты отрефлексируешь свой опыт ее использования и только тогда тебя догоняет понимание того, как же ее нужно использовать, чтобы это было и полезно, и не сложно, и не приводило к проблемам, о которых ты раньше и не знал. С некоторыми фичами эта история растягивается не на один год. Вот как у меня с концептами, например. А до этого похожая история была с noexcept.

четверг, 7 марта 2024 г.

[work.culture] Использование жестких, на грани грубости, оценок в работе

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

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

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

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

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

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

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

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

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

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

Мат (если без него не обойтись) должен использоваться только в исключительных случаях.

И ни в коем случае нельзя использовать мат в отношении подчиненных. Да, я понимаю, что временами очень тяжело удержаться от вопроса "ты что долбо*б?" или "а не ох*ел ли ты?", но нужно. Да и клиентов, в их отсутствие, так же лучше матами не обкладывать. И не только потому что везде есть уши 🙂

Т.е. если мат используется эпизодически и к месту, то это еще терпимо. Хотя лучше бы вообще без... Но жизнь есть жизнь.

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

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


В общем, к чему это я?

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

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

понедельник, 4 марта 2024 г.

[prog.flame] Пример серьезного, на мой взгляд, просчета в API библиотеки libconfig

Впервые довелось столкнуться с библиотекой libconfig. Довольно таки популярной, насколько я могу судить. Тем удивительнее было обнаружить там описанный ниже косяк.

Чтобы получить значение целочисленного параметра нужно использовать функцию config_setting_get_int:

int config_setting_get_int (const config_setting_t * setting)
long long config_setting_get_int64 (const config_setting_t * setting)

These functions return the value of the given setting. If the type of the setting does not match the type requested, a 0 value is returned.

Т.е. если мы пытаемся получить значение параметра, а нам возвращают 0, то этот ноль может означать:

  • что значение не получено, т.к. оно имеет другой тип. Например, вместо my_param=0 задано my_param="0" или my_param="zero";
  • что значение получено и это таки ноль. Просто ноль.

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

Аналогичная проблема есть и с семейством функций config_lookup_int/config_lookup_int64 и прочих вариантов lookup-чего-то-там. Эти функции возвращают CONFIG_FALSE и в случае, если параметр вообще не был найдет, и в случае, если был найден, но содержит не тот тип (например, строка вместо числа).

При этом, насколько я могу судить по коду libconfig, в случае несовпадения типа для значения libconfig даже errno не меняет.

Т.е. получив 0 из config_setting_get_int или CONFIG_FALSE из config_lookup_int у меня нет возможности разобраться с тем есть ли ошибка и, если есть, какая она.

Хотя, как по мне, избежать этой проблемы можно было бы очень просто, если бы у config_setting_get_int был другой формат:

int config_setting_get_int(
  // Где искать значение.
  const config_setting_t * setting,
  // Куда помещать значение.
  int * value_receiver)

И возвращаемое значение означало бы признак успешности, вроде такого: 0 -- все OK, -1 -- значение имеет другой тип, -2 -- значение слишком большое, чтобы уместиться в int и т.д.

Очевидная, вроде бы, вещь. Но почему-то не сделанная... 🙁