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

О блоге

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

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

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

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

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

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

понедельник, 28 сентября 2026 г.

[life.photo] Несколько кадров из прогулки по лесу

Давненько здесь не было ничего про фотографию. Но в минувшие выходные прогулялся по лесу со своей старенькой беззеркалкой Fujifilm x30, что-то получилось снять. И что-то из отснятого даже понравилось. Поэтому решил поделиться несколькими снимками с небольшими комментариями. Хотя, если фотография нуждается в комментариях, то стоимость ее невелика, однако в данном случае речь не столько о самих кадрах, сколько про мое отношение к отснятому. В общем, кому интересно, милости прошу под кат.

воскресенье, 27 сентября 2026 г.

[comp.thoughts] Многопроцессорные ноутбуки на ARM-ах. Почему нет?

Уже почти два года с удовольствием пользуюсь 12" планшетом HONOR Pad 9. По Geekbench 7 у него не сказать, чтобы какие-то выдающиеся результаты. Что-то около 800 баллов в однопотоке и чуть больше 2800 баллов в многопотоке. Т.е. формально это откровенно тормознутый по нынешним меркам процессор.

Однако, за два года использования этого планшета для потребления контента (т.е. ролики на YouTube/RuTube/VK, чтение статей на сайтах, просмотр PDF, небольшие правки в Google Docs, простые ответы по email, переписка в Telegram, Yandex.Music в фоне и вот это вот все) никаких тормозов замечено не было. Т.е. с производительностью для обычных повседневных задач у достаточно старого по нынешним временам Snapdragon 6 Gen 1 хватает. И здесь остается только жалеть, что на таком процессоре нет легкого, тихого и долго живущего на одном заряде ноутбука.

Почему раньше не было ноутбуков на ARM-ах из мобильных телефонов понятно: хоть Windows когда-то в 1990-е и начиналась как ОС с поддержкой нескольких типов архитектур, но довольно быстро десктопная версия Windows сконцентрировалась исключительно на x86, а всякие MIPS и Alpha пошли лесом, т.к. рыночек порешал. Этот же рыночек вместе с эффективным менеджментом из Microsoft и Nokia порешал и мобильную Windows CE (или как она там в последние годы своей жизни назвалась).

Тем не менее, мегауспешный переход Apple на свои собственные процессоры подтолкнул Microsoft к портированию десктопной Windows на ARM-ы. И уже года четыре как Windows доступна на ноутбуках с ARM. Например, был вот такой совсем чахлый TCL Book 14 Go на Snapdragon 7C Gen.1, который, по отзывам, был не просто тормозной, а очень тормозной. Возможно, не из-за процессора, а совсем крошечного по нынешним временам объема RAM, но все таки.

Первый блин Windows-ноутбуков на ARM в лице вот таких вот TCL Book 14 Go вышел комом, поэтому дальше эволюция пошла привычным для ИТ способом -- дождались выхода гораздо более мощных Snapdragon X от Qualcomm и пошли клепать не самые дешевые модели с этим новым процессором, большим объемом памяти, крутыми экранами и вот этим вот всем. Т.е. как будто бы основной прицел был в сегмент повыше среднего. Встречаются недорогие ASUS Vivobook-и и Lenovo IdeaPad-ы из среднего сегмента, но там и разновидности Snapdragon X не самые мощные.

Уж не знаю, есть ли какой-то успех у Windows-ноутбуков на ARM, сам пока не отважился на покупку чего-то подобно (хотя, наверное, надо бы рискнуть, чтобы было на чем проверять свои OpenSource разработки). Но давеча задумался вот о чем: а почему не пробуют сделать ARM ноутбуки с несколькими процессорами?

Грубо говоря, почему бы вместо дорогих и горячих Snapdragon X не взять 2-3-4 более старых, но вполне себе годных Snapdgragon 6 Gen 1, как в моем HONOR Pad 9? Пусть каждый из них сидит в своем сокете и располагает собственными 6 или 8 GiB RAM.

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

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

Что-то мне кажется, что не надо.

Смысл же в том, что если сделать ноутбук на одном Snapdragon 6 Gen 1 с 8 GiB RAM, то его ограниченность (как в плане быстродействия, так и в плане доступной памяти) быстро выплывет на поверхность.

Но есть таких процессоров будет уже два, а памяти не 8, а 16, то это уже совсем другая история. А если не 2, а 3 процессора и 24 GiB RAM? Это же еще лучше. Ну а 4-е процессора -- это уже совсем серьезная штука 😀

Любопытно, почему по этому пути не пошли? Что является основным препятствием? Железо или софт?

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

пятница, 25 сентября 2026 г.

[prog.question] А что сейчас понимают под "архитектурой программного обеспечения"?

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

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

Как бы вопрос не праздный, поскольку последние лет 10, а может и побольше, мне приходится, в основном, колупаться в коде: искать проблемы, что-то переписывать, что-то дописывать, что-то выбрасывать и т.д, и т.п. Но уже давно не доводилось заниматься проектированием "по большому", типа того, что здесь у нас будет такая-то СУБД и в ней будет храниться то-то и то-то, здесь у нас будет такой компонент, который с БД будет общаться напрямую, а здесь вот такой компонент, который будет общаться не с БД, а с первым компонентом, а здесь у нас будет вот такой компонент, который будет вот так-то следить за первыми двумя компонентами, а здесь будет еще вот это и вот это... В последние годы если что-то и приходится проектировать, так это небольшие библиотеки или даже отдельные классы, которые упрощают те или иные задачи.

Складывается впечатление, что под "архитектурой ПО" подразумевается рисование квадратиков и связей между ними, как это принято делать на system design interview, где соискателя просят "спроектировать" очередной клон Facebook-а или YouTube.

Если я прав, то непонятно почему кто-то надеется, что ИИ не заберет эту задачу у человека точно так же, как забирает обычное кодирование.

В общем, кому не жаль времени, просвятите, пожалуйста, старого пердуна отставшего от трендов лисапедостроителя, что же сейчас попадает под термин "архитектура ПО" и понимается под "проектирование архитектуры"?

понедельник, 14 сентября 2026 г.

[prog.thoughts] Мои личные заморочки с ранними return-ами

Прежде чем перейти к основной теме поста два важных дисклеймера:

  • во-первых, это мое личное мнение. Делюсь им, но ни в коем случае не настаиваю на его непогрешимости и, уж тем более, не утверждаю, что оно единственно правильное. Может быть кто-то из прочитавших сочтет его заслуживающим внимания и постарается относиться к return-ам более ответственно, чтобы таким как я было проще читать код;
  • во-вторых, я программирую, в основном, на C++, поэтому ниже будет C++ная специфика. Возможно, сказанное ниже гораздо менее критично для других языков программирования;
  • в-третьих, любой анонимный эксперт c LOR, RSDN или Habr вам убедительно объяснит, что программировать я не умею, да и вообще редкостный дебил. Посему ошибки у меня сплошь и рядом, в том числе и с плюс-минус-единичкой.

Итак, когда я только начинал учиться программированию в 1990-ом году, то нам говорили, что умные люди масштаба Никлауса Вирта рекомендуют иметь единственный return в функции. Типа одна точка входа -- одна точка выхода.

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

Очевидный пример -- это проверка валидности аргументов функции/метода:

int copy_file(const std::string & src, const std::string & dest)
{
  if(src.empty()) // Ошибка: нет имени источника.
    return errors::empty_src_name;

  if(dest.empty()) // Ошибка: нет имени результата.
    return errors::empty_dest_name;

  ... // Далее уже непосредственно действия по копированию.
}

Этот код, имхо, гораздо понятнее, чем вот такой:

int copy_file(const std::string & src, const std::string & dest)
{
  int result = errors::unexpected_error;

  if(!src.empty())
  {
    if(!dest.empty())
    {
      ... // Далее уже непосредственно действия по копированию.
    }
    else // Ошибка: нет имени результата.
      result = errors::empty_dest_name;
  }
  else // Ошибка: нет имени источника.
    result = errors::empty_src_name;

  return result;
}

Или вот такой:

int copy_file(const std::string & src, const std::string & dest)
{
  int result = errors::unexpected_error;

  if(src.empty()) // Ошибка: нет имени источника.
    result = errors::empty_src_name;
  else if(dest.empty()) // Ошибка: нет имени результата.
    result = errors::empty_dest_name;
  else
  {
    ... // Далее уже непосредственно действия по копированию.
  }

  return result;
}

Еще один простой пример: отказ от выполнения действий, если объект находится в неподходящем состоянии. Допустим, у нас есть класс "Файл" в виде обертки над простым хэндлом, который записывает значения строго в BigEndian представлении. Что-то вроде:

class file_with_big_endian_data
{
  // Хэндл открытого файла. Значение -1 указывает, что файл не был открыт.
  int m_handle;
  ...
public:
  // Методы для записи различных типов значений.
  void write(std::span<const std::uint16_t> what);
  void write(std::span<const std::uint32_t> what);
  void write(std::span<const std::uint64_t> what);
  ...
};

В таком случае каждый метод write может иметь в начале проверку m_handle с ранним return-ом для того, чтобы ничего не делать, если файл не был открыт:

void file_with_big_endian_data::write(
  std::span<const std::uint16_t> what)
{
  if(-1 == m_handle)
    return;

  ... // Запись с перестановкой байт, если это необходимо.
}

Еще один пример подсказали в комментариях на LinkedIn: ранние return-ы для ситуаций, когда компилятор может избавиться от хвостовой рекурсии. Сам пример достаточно игрушечный, но для иллюстрации вполне себе норм. Привожу его в оригинале на Python-е (спасибо пользователю Konstantin Z.):

def classify(n, steps=0):
  if n < 0:
    return "negative"
  if n == 0:
    return f"reached zero in {steps} steps"
  if n == 1:
    return f"almost there, {steps + 1} steps"
  return classify(n - 2, steps + 1)

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

Прежде всего лично мне гораздо удобнее разбирать прикладную логику в виде лесенки if-ов. Т.е. вот это:

if(first-condition)
{
  if(second-condition)
  {
    if(third-condition)
    {
      ... основные действия, ради которых все условия проверялись.
    }
  }
}

изучать гораздо проще, чем вот это:

if(!first-condition)
  return;

if(!second-condition)
  return;

if(!third-condition)
{
  ... основные действия, ради которых все условия проверялись.
}

Видимо это какие-то особенности моего восприятия (или проявления старческого слабоумия). Но лесенка из if-ов как бы сама уводит взгляд к кульминации и сама говорит "если вот это, и вот это, и вот это, то..." Тогда как ранние return-ы эту прямую последовательность к кульминации прерывают.

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

А вот если уходящую вправо лесенку из if-ов переписать в виде нескольких if-ов с return-ами, то лично для меня логика уже меняется и воспринимать замысел автора становится сложнее.

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

Тогда как с ранними return-ами такого критерия нет. Может быть функция на 40 строк, в которой 10 ранних return-ов. По сути это уже говнокод, но без столь явного запаха, как у лесенки из if-ов в 5-6 уровней глубины.

Но хуже всего, когда встречаются объемные функции, в которых ранние return-ы расположены в if-ах на разной глубине. Что-то вроде:

int do_something_important(int a, int b)
{
  if(!is_first_arg_valid(a))
    return results::error_code_1;

  if(!is_second_arg_valid(b))
    return results::error_code_2;

  bool need_update_cache = false;
  if(is_specific_arg_combination(a, b))
  {
    const auto cache_id = make_cache_id(a, b);
    if(has_result_in_cache(cache_id))
    {
      take_value_from_cache(cache_id);
      return results::result_from_cache;
    }
    else
      need_update_cache = true;
  }

  int prepare_result = prepare_for_change(a, b);
  if(prepare_result != results::ok)
  {
    if(prepare_result == results::pool_overflow)
    {
      remove_oldest_from_pool();
      prepare_result = prepare_for_change(a, b);
    }
    if(prepare_result != results::ok)
      return prepare_result;
  }

  commit_change(a, b);
  if(need_update_cache)
  {
    const auto cache_id = make_cache_id(a, b);
    const auto cache_update = update_cache_for(cache_id);
    if(cache_update == results::no_resources)
      return results::ok;
    else
      return results::ok_with_warnings;
  }

  return results::ok;
}

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

Еще хуже, когда в центре такой объемной функции будет цикл, внутри которого будут сочетаться и return, и break, и continue. И если мне скажут, что это перебор и такого не бывает, то увы, такое бывает. Хотя да, это перебор.

Но помимо чтения подобного кода иногда у меня возникает необходимость временно добавить в подобную функцию трассировку. В простейшем случае это может быть печать "do_something_important started" при входе и "do_something_important completed" при входе.

Когда у нас есть единственный return в конце, то нет ничего проще -- одна печать на входе, вторая на выходе. Но когда return-ов много, то...

...то приходится прибегать к созданию в начале функции объекта, который в своем конструкторе будет печатать "do_something_important started", а в деструкторе -- "do_something_important completed". Для языков, в которых детерминированного вызова конструкторов-деструкторов нет, подойдет та или иная реализация Go-шного defer-а.

Но это не помогает, если строчка с "do_something_important completed" должна содержать еще и какое-то значение (например, возвращаемое значение). Вставлять такую печать перед каждым return-ом ну так себе занятие. Приходится что-то изобретать.

А вот для единственного return-а это не проблема от слова совсем. Тут вообще все тривиально и ничего не нужно изобретать.

Ну и как-то мне с необходимостью делать подобные трассировки регулярно везет. То нужно построить трассу взаимных вызовов. То засечь время работы. Или и то, и другое сразу. Благо в C++ легко делать объекты в RAII-стиле. В какой-нибудь Java сложностей было бы побольше.


Что хочется сказать в завершении.

ИМХО, в современном мире правило "один вход -- один выход" -- это уже какой-то экстремальный экстрим. Непонятно зачем отказываться от ранних return-ов, если они делают код и компактнее, и проще, и надежнее (как элемент defensive programming), и (временами) быстрее (раскрутка хвостовой рекурсии).

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

Посему могу только попросить программистов думать при написании return-ов, чтобы не плодить их сверх меры.

суббота, 5 сентября 2026 г.

[prog.c++] Всегда удивляюсь, когда вижу имена членов класса без суффиксов/префиксов

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

class Foo {
    int value;
public:
    Foo(int value) { // параметр затеняет поле class
        value = value; // ничего не делает, присваивает параметр самому себе
    }
};

Честно говоря, мне дико видеть, что кто-то до сих пор использует имена членов класса без специальных префиксов/суффиксов. Вроде как очень и очень старый прием, которым я сам пользуюсь с середины 1990-х и который не только делает код читабельнее, но и защищает от подобных ошибок.

У меня бы было написано так:

class Foo {
    int m_value;
public:
    Foo(int value) { // параметр ничего не затеняет
        m_value = value;
    }
};

И подобная опечатка сразу бы оказалась видна.

Но если уж префиксы или суффиксы для членов класса оскорбляют ваше чувство прекрасного, то возьмите за правило обращаться к ним через this->:

class Foo {
    int value;
public:
    Foo(int value) { // параметр затеняет поле class
        this->value = value; // ничего не затеняет
    }
};

Тем паче, что обращение this-> зачастую бывает необходимо при написании кода шаблонов (например, когда шаблон класса наследуется от шаблона класса и обращается к унаследованным из базового класса членам/методам).

PS. Самая странная система именования членов классов, которую доводилась видеть, была такой:

class Demo {
public:
  int a; // Открытый член класса без префикса или суффикса.

protected:
  int _b; // А вот защищенные и закрытые члены с префиксом.

private:
  int _c;
};

PPS. Касательно самой статьи "Анатомия граблей": она позиционируется как сборник C++ных граблей из области игростроя. Но практически все из описанного мне доводилось видеть в разнообразных проектах, которые к игрострою не имели никакого отношения. И какие-то из них (вроде выхода за границы статических массивов) не имеют дешевых решений, увы. По поводу же той части статьи, которая касается контроля за ресурсами и ручной работы с malloc/free, то слишком мало конкретики (плюс есть у меня подозрение, что в игрострое собирается публика, которой очень и очень фиолетово качество кода, но не факт, что подозрение верное).