Ценность квалифицированного программиста смещается в сторону умения проводить обзоры кода. Генерировать код становится проще, но всё так же важно проверять его с точки зрения качества декомпозиции, корректности реализации, эффективности, безопасности. Посмотрим на примере маленького проекта markus, созданного с помощью Claude Opus, почему важно понимать сгенерированный код и уметь видеть, что скрывает красивый текст программы.
Проект markus
Я искал какой-то проект, сгенерированный с помощью ИИ, и мне попался markus.
Это single-header библиотека для парсинга текста и конвертации его в HTML. Создана с помощью Claude Opus 4.5.
Мне понравилось, что библиотека крошечная. Можно её просмотреть и даже погрузиться в изучение отдельных фрагментов кода. Если перед вами сразу много вайб-кода, то как-то даже не хочется начинать его смотреть.
Библиотека состоит из одного файла markus.h, содержащего 6484 строки на C++20. По существу кода ещё меньше. Некоторые алгоритмы в целях оптимизации используют табличные методы. Четыре таблицы представлены в коде бессмысленно вытянуто. Пример:
static constexpr uint8_t kWhitespaceTable[256] = { 0, 0, 0, 0, .... // и так 256 строк
Если таблицы не считать, то, собственно, кода ещё на 1000 строк меньше. Анализатор PVS-Studio ничего особенного в проекте не нашёл, но проверка стала поводом посмотреть некоторые фрагменты поподробнее. И вот уже специалист, то есть я, могу выделить моменты, которые стоит обсудить.
Проблематика вайб-кодинга
Генерация кода с помощью ИИ может очаровать, особенно неискушённого программиста, который не работал с большими старыми проектами.
Кто сталкивался, тот понимает, что главная задача — не написать код, а сделать так, чтобы его можно было сопровождать и развивать. Об этом хорошо написал Роберт Мартин:
Без присмотра AI будет нагромождать код поверх кода. У него нет чувства большой картины. Архитектура, вероятно, за пределами его возможностей.
Так что изучение принципов декомпозиции, умение правильной постановки задач и т. д. становятся только более актуальной компетенцией программиста.
Но я хочу посвятить статью другой особенности вайб-кодинга, про которую мало говорят: это очарование процессом генерации кода и, как следствие, излишнее доверие к нему. Редкое исключение — заметка “ИИ пишет код быстрее. Но не понимает, что пишет”.
Убедившись, что простые задачи решаются хорошо или даже лучше, чем это бы сделали вы сами, теряется критичность взгляда. Кажется, что и в больших задачах качество будет высоким, код оптимизированным и т. д.
За примером далеко ходить не надо. Я ради интереса просил ИИ написать код для быстрого подсчёта количества единичных бит в больших массивах. Я был очарован предложенными алгоритмами. Про многие интересные решения я даже не знал, хотя в своё время прочитал в книге разбор целого ряда алгоритмов на эту тему.
Смотришь на предложенный код подсчёта бит и думаешь: “А не пора ли податься в фермеры?” Сам бы я такой код не написал и не догадался бы его поискать, т. к. просто не знаю про такие алгоритмы.
Это восхищение проецируется на весь код, созданный ИИ. Когда я только заглянул в код markus, то подумал, что анализировать его смысла нет: всё красиво, оптимизированные алгоритмы и т. д:
-
Тонкая настройка оптимизации компилятора с помощью подсказок наиболее вероятных веток исполнения [[unlikely]];
-
Использование условной компиляции на основе
if constexpr; -
Использование оптимизированных табличных алгоритмов;
-
Генерация SIMD-Friendly Helper Functions;
-
Использование контейнеров из пространства имён
std::pmrдля задания собственной стратегии выделения памяти.
Код солиден, что там смотреть? Оказывается, есть что. Ощущение, что код оптимизирован, — ложное. Вернее, местами он действительно оптимизирован, а местами всё плохо.
К чести автора проекта хочу сказать, что он молодец и взвешенно подходит к описанию проекта. Райан явно предупреждает:
DO NOT USE IN PRODUCTION. This is completely vibe coded and has not undergone any reviews for memory safety. Its performance is also 2-3x slower than cmark.
Какую проблему я хочу подсветить статьёй? Код может выглядит педантичным, оптимизированным и безопасным, но таковым не является.
Хорошо, что работу проекта markus можно сравнить с другим решением и понять, что он получился не очень быстрым. А если сравнивать не с чем? Нужно экспертное мнение человека. Беда в том, что многим кажется, что теперь эксперт и обзор кода не нужны.
Нужен ли эксперт?
А может быть, оценка кода экспертом действительно не нужна?
Вы создали код. Он выглядит красиво и опрятно. Он выглядит оптимизированным и работает. Может, этого достаточно?
На мой взгляд, без оценки можно обойтись, если выполняются все четыре условия:
-
Никто не пострадает и не понесёт убытки, если код работает не так, как задумывалось, из-за багов или неправильной интерпретации ТЗ.
-
Код безответственный в плане безопасности. Никто не будет его ломать, так как это никому не нужно.
-
Скорость работы кода неважна.
-
Не предполагается, что у проекта будет долгий жизненный цикл. Его не потребуется развивать десятилетиями, сопровождать, адаптировать для других платформ и т. д.
Во всех остальных случаях, извините, все требования к проверке качества кода в силе и ещё более актуальны. К вайб-коду должны применяться все те же процессы, что описываются в SSDLC / стандарте РБПО.
Почему я строг к ИИ
Я не строг, а требователен. Для реализации проекта взят язык С++. Суть использования C++ — построение быстрых, эффективных приложений, экономно использующих память. Если вам не нужна быстрая программа, не надо использовать C++.
Раз уж выбран C++ для создания быстрого приложения, то задача именно написать быстрый код. Неважно, пишет его человек или это делает ИИ. Если быстрый код не получился — задача не решена.
Мой личный интерес — изучать, что такое вайб-код и как статические анализаторы могут помочь контролировать его надёжность. Вайб-коду, как и людям, свойственно антипаттерны, но другие. Есть смысл развивать анализатор PVS-Studio с учётом этих особенностей.
Попутно, раз я всё равно смотрю проекты, есть повод написать статью. У меня нет задачи ругать вайб-код и в целом вайб-кодинг. Это глупо, это просто инструмент. Но в отличие от евангелистов ИИ у меня нет задачи продвигать эти технологии, поэтому если я вижу, что код — хрень, я так и напишу.
SIMD-friendly код
Комментарий к функции IsSpanBlank обещает, что код составлен из расчёта облегчить компилятору задачу его векторизации, например, с помощью SSE-инструкций.
Суть оптимизации — несколько элементов обрабатываются одновременно, чтобы определить, хранят ли они пробельные символы.
// Check if a span contains only blank characters (space, tab, \r, \n)// SIMD-friendly: processes 8 bytes at a timeinline bool IsSpanBlank(const char* data, size_t len) { size_t i = 0; // Process 8 bytes at a time for (; i + 8 <= len; i += 8) { // For each byte, check if it's NOT a blank character // Blank chars: space(0x20), tab(0x09), \n(0x0A), \r(0x0D) bool all_blank = true; for (size_t j = 0; j < 8; ++j) { unsigned char c = static_cast<unsigned char>(data[i + j]); bool is_blank = (c == ' ' || c == '\t' || c == '\n' || c == '\r'); all_blank = all_blank && is_blank; } if (!all_blank) return false; } // Handle remaining bytes for (; i < len; ++i) { unsigned char c = static_cast<unsigned char>(data[i]); if (c != ' ' && c != '\t' && c != '\n' && c != '\r') { return false; } } return true;}
Код создаёт впечатление, что ИИ знает, что делает. Внутренний цикл всегда обрабатывает по 8 символов, и предполагается, что эта часть может быть оптимизирована с помощь SIMD-инструкций. Оставшийся хвост символов обрабатывается отдельным простым циклом.
Мне этот код показался подозрительным тем, что на самом деле байты обрабатываются всё так же последовательно. Вложенный цикл не несёт чего-то полезного. На мой взгляд, этот код не лучше и не хуже простейшего варианта его реализации.
inline bool IsSpanBlank_Simple(const char* data, size_t len) { for (size_t i = 0; i < len; ++i) { unsigned char c = static_cast<unsigned char>(data[i]); if (c != ' ' && c != '\t' && c != '\n' && c != '\r') { return false; } } return true;}
Я бы написал именно такой код. Если бы я понимал, что это узкое место и нужно непременно сделать код как можно быстрее, я бы пошёл искать решение, построенное на интринсиках.
Какой-то другой вариант, например, с вложенным циклом, я бы делать не решился. Я хоть что-то и понимаю в оптимизации, но не настолько, чтобы отважиться писать хитрый код с целью подсказать компилятору.
Можно сказать: “Вот! Ты слаб! А Claude Opus взял и сделал. Взял и написал более умный вариант”.
Давайте попробуем сравнить скорость. Мой коллега Филипп провёл небольшой эксперимент с GCC и Clang:
-
Формируем одинаковый набор данных для обоих бенчмарков: генерируем 1 000 000 строк случайной длины от 1 до 127 символов с произвольным содержимым. Генерация наборов не учитывается в замерах.
-
Подаём на вход алгоритму набор данных и для каждой строки вычисляем, содержит ли она пробельные символы. Замеряем общее время.
В обоих вариантах код компилируется с агрессивными оптимизациями -O3. Можно заметить, что для обоих компиляторов простой вариант функции лучше, чем с вложенным циклом.

Быть может, просто надо какой-то волшебный ключ оптимизации указать, чтобы вайб-код оптимизировался лучше? Нет такого ключа. А если будет, компилятор и без подсказки сам сделает столь же эффективный вариант из простого кода с одним циклом.
Параллельно с коллегой тоже поэкспериментировал с двумя рассмотренными и тремя новыми вариантами алгоритма. Я использовал компилятор MSVC. Давайте посмотрим, какие результаты замеров получились.
Во-первых, мне подсказали, что ситуацию можно улучшить, заменив в изначальном варианте || на |. Short-circuit evaluation в нашем случае не помогает, а только вредит. Улучшенная проверка:
bool is_blank = (c == ' ' | c == '\t' | c == '\n' | c == '\r');
Полный код функции
inline bool IsSpanBlank_BinOR(const char* data, size_t len) { size_t i = 0; // Process 8 bytes at a time for (; i + 8 <= len; i += 8) { // For each byte, check if it's NOT a blank character // Blank chars: space(0x20), tab(0x09), \n(0x0A), \r(0x0D) bool all_blank = true; for (size_t j = 0; j < 8; ++j) { unsigned char c = static_cast<unsigned char>(data[i + j]); bool is_blank = (c == ' ' | c == '\t' | c == '\n' | c == '\r'); all_blank = all_blank && is_blank; } if (!all_blank) return false; } // Handle remaining bytes for (; i < len; ++i) { unsigned char c = static_cast<unsigned char>(data[i]); if (c != ' ' && c != '\t' && c != '\n' && c != '\r') { return false; } } return true;}
Во-вторых, я спросил DeepSeek. Он предложил вариант оптимизации, построенный на битовых операциях:
inline bool IsSpanBlank_Deepseek(const char* data, size_t len) noexcept { // Создаём битовую маску для допустимых символов // Используем 128-битную маску для ASCII static constexpr uint64_t blank_mask_low = []() { uint64_t mask = 0; mask |= 1ULL << ' '; mask |= 1ULL << '\t'; mask |= 1ULL << '\n'; mask |= 1ULL << '\r'; return mask; }(); for (size_t i = 0; i < len; ++i) { unsigned char uc = static_cast<unsigned char>(data[i]); if (uc >= 64) return false; // Все blank-символы в ASCII < 64 if (!((blank_mask_low >> uc) & 1)) return false; } return true;}
И напоследок я попросил у DeepSeek оптимизированную функцию, по-настоящему использующую векторизацию. Вот что получилось:
inline bool IsSpanBlank_Deepseek_SSE(const char* data, size_t len) noexcept { // Создаём векторы с допустимыми символами const __m128i space = _mm_set1_epi8(' '); const __m128i tab = _mm_set1_epi8('\t'); const __m128i newline = _mm_set1_epi8('\n'); const __m128i carriage = _mm_set1_epi8('\r'); size_t i = 0; // Обрабатываем по 16 байт for (; i + 15 < len; i += 16) { __m128i chunk = _mm_loadu_si128(reinterpret_cast<const __m128i*>(data + i)); // Проверяем, что каждый байт является одним из допустимых __m128i eq_space = _mm_cmpeq_epi8(chunk, space); __m128i eq_tab = _mm_cmpeq_epi8(chunk, tab); __m128i eq_nl = _mm_cmpeq_epi8(chunk, newline); __m128i eq_cr = _mm_cmpeq_epi8(chunk, carriage); __m128i is_blank = _mm_or_si128(_mm_or_si128(eq_space, eq_tab), _mm_or_si128(eq_nl, eq_cr)); // Получаем маску и проверяем int mask = _mm_movemask_epi8(is_blank); if (mask != 0xFFFF) return false;} // Обрабатываем остаток for (; i < len; ++i) { char c = data[i]; if (c != ' ' && c != '\t' && c != '\n' && c != '\r') return false; } return true;}
Теперь результаты. Хотя 32-битные сборки сейчас неактуальны, ради интереса я и её запустил. Замеры выполнялись на массиве размером 212 мегабайт, заполненном случайными пробельными символами. Выполнение потока привязывается к одному предварительно прогретому ядру. Если непонятно, о чём идёт речь, загляните в статью “Вечный вопрос измерения времени”.
Win32:1. Самый простой вариант с одним циклом: 1.39 сек.2. Изначальный "оптимизированный код" с вложенным циклом: 1.86 сек.3. В изначальном варианте операторы || заменены на | : 0.351 сек.4. Вариант от Deepseek с оптимизацией на битовых масках: 0.817 сек.5. Вариант от Deepseek с оптимизацией на SSE2 интристиках: 0.0449 сек.Win64:1. Самый простой вариант с одним циклом: 0.175 сек.2. Изначальный "оптимизированный код" с вложенным циклом: 0.309 сек.3. В изначальном варианте операторы || заменены на | : 0.253 сек.4. Вариант от Deepseek с оптимизацией на битовых масках: 0.207 сек.5. Вариант от Deepseek с оптимизацией на SSE2 интристиках: 0.0396 сек.
Хорошо заметно, что 64-битный код получился намного эффективнее, кроме случая, где в ход идёт SSE2, но и там в любом случае всё быстро.
Код, сгенерированный Claude Opus для проекта markus, худший из всех вариантов.
Даже самый простой вариант с обыкновенным циклом не только быстрее, но ещё и короче. Дополнительные строки кода пошли только во вред.
В лидерах код на интристиках, но платой тому — ограничение переносимости.
Кто-то скажет, что я зря переживаю. Да, Claude Opus выбрал неудачно решение, но ведь DeepSeek предложил вполне хорошее, быстрое и переносимое решение.
Согласен. Вот только в проекте мы видим неудачный код. Код выглядит оптимизированным, но таковым не является. Задача не решена. Нужен эксперт, который это поймёт и выберет другое решение.
В будущем проблема будет нарастать. Сгенерированного кода будет всё больше, а людей, которые могут делать обзор кода, останется столько же. Затем системы начнут учиться на таком же “оптимизированном” коде… Ох, интересные времена нас ждут.
Сравнение строк: цветочки
Моё внимание привлекло предупреждение PVS-Studio: V590 [CWE-571] Consider inspecting this expression. The expression is excessive or contains a misprint. markus.h 5987
Действительно, код содержит избыточную проверку:
BlockNode ParseParagraph() { .... auto trimmed = detail::TrimLeft(line); // trimmed имеет тип std::string_view .... if (trimmed.size() >= 2 && trimmed[1] == '!' && trimmed.size() >= 9) { ....}
Тут, на самом деле, ничего страшного. Ну лишняя проверка, которую и так выкинет компилятор. Для красоты код, конечно, стоит поправить, но не более того:
if (trimmed.size() >= 9 && trimmed[1] == '!') // вот теперь хорошо
Однако глаза скользнули на код ниже if. И вот там избыточность, которая достойна вашего внимания:
std::pmr::string upper;for (size_t j = 0; j < 9; ++j) { upper += static_cast<char>( std::toupper(static_cast<unsigned char>(trimmed[j])));}if (upper.starts_with("<!DOCTYPE")) { goto html_interrupt;}
Здесь выполняется проверка, что строка начинается с "<!doctype", причём без разницы, используются строчные буквы или заглавные. Чтобы игнорировать верхний/нижний регистр, создаётся временная строчка в верхнем регистре и уже она сравнивается с шаблоном.
Так себе решение с точки зрения эффективности. Создание временной строки — это тяжёлая операция, несмотря на то, что используется std::pmr::string. Всё равно ведь надо символы копировать в память, да ещё по одному. Кстати, здесь простой std::string будет не хуже, так как строка не более 15 символов и сработает Small String Optimization.
Лучше написать функцию, выполняющую сравнение без учёта регистра. Например, такую:
bool starts_with_insensitive(std::string_view str, std::string_view prefix_lower_case){ if (str.size() < prefix.size()) return false; const auto pred = [](unsigned char lhs, unsigned char rhs) { return lhs == std::tolower(rhs); }; return std::equal(prefix_lower_case.begin(), prefix_lower_case.end(), str.begin(), pred);}
Это просто пример. Можно реализовать алгоритм по-другому или сделать istring_view, написав свои char_traits, и так далее. Главное — мы избавляемся от создания временной строки и упрощаем код, выполняющий проверку:
if (starts_with_insensitive(trimmed, "<!doctype")) { goto html_interrupt;}
Ещё ярче необходимость создания функции сравнения станет видна в следующей главе. Как говорится, это только цветочки, а ягодки будут впереди.
Сравнение строк: ягодки
Просматривая другие предупреждения PVS-Studio, я обратил внимание на этот вектор строк:
std::optional<HtmlBlock> TryParseHtmlBlock() { .... static const std::pmr::vector<std::pmr::string> type1_tags = { "script", "pre", "style", "textarea" }; ....}
Анализатору он не нравится: V1096 The ‘type1_tags’ variable with static storage duration is declared inside the inline function with external linkage. This may lead to ODR violation. markus.h 4781
Нет смысла рассматривать это предупреждение в статье. Куда серьёзнее то, как работают с этим вектором. Анализатор не может оперировать высокоуровневыми понятиями и заметить неладное, зато это может заметить человек и сказать всё, что думает про эту дичь.
Код ниже проверяет наличие этих тегов в строке. Но ведь в теге ещё есть открывающая угловая скобка <, поэтому ИИ, не стыдясь, создаёт эти временные строки, содержащие тег со скобкой:
for (const auto& tag : type1_tags) { std::pmr::string open_tag = "<" + tag;
Далее нужно проверить наличие этого тега в строке без учёта регистра. Собственно, почему бы для этого не создать ещё одну временную строчку. Вайб-код генерировать — не мешки ворочать.
std::pmr::string lower;for (size_t j = 0; j < open_tag.size(); ++j) { lower += static_cast<char>( std::tolower(static_cast<unsigned char>(trimmed[j])));}
Весь этот кринж выглядит следующим образом:
static const std::pmr::vector<std::pmr::string> type1_tags = { "script", "pre", "style", "textarea"};for (const auto& tag : type1_tags) { std::pmr::string open_tag = "<" + tag; if (trimmed.size() >= open_tag.size()) { std::pmr::string lower; for (size_t j = 0; j < open_tag.size(); ++j) { lower += static_cast<char>( std::tolower(static_cast<unsigned char>(trimmed[j]))); } if (lower == open_tag) { // .... } }}
В худшем случае (если тега в тексте нет), создаётся восемь временных строк! Ситуацию лишь немного спасает то, что формируемые строки небольшие по размеру, и Small String Optimization внутри std::pmr::string устранит динамические аллокации.
И ведь можно достаточно легко сделать код быстрее. Во-первых, можно сразу сформировать нужные префиксы и записать их в вектор. Во-вторых, вектор можно смело заменить на массив std::string_view, что вовсе уберёт динамические аллокации.
using namespace std::literals;static constexpr std::array type1_tags = { "<script"sv, "<pre"sv, "<style"sv, "<textarea"sv};
В-третьих, теперь можно найти префикс с помощью написанной ранее функции starts_with_insensitive. В результате весь говнокод, приведённый выше, сокращается до:
using namespace std::literals;static constexpr std::array type1_tags = { "<script"sv, "<pre"sv, "<style"sv, "<textarea"sv};for (auto tag : type1_tags) { if (starts_with_insensitive(trimmed, tag.c_str(), tag.size()) {
Код стал в два раза короче и в N раз быстрее.
Можно и дальше улучшения делать. Например, не создавать строку с тегом в скобах end_condition = "</" + tag + ">";, а брать уже готовую из массива. Но хватит уже это место мучить.
Цена красоты
Когда я первый раз пролистывал код, моё уважение вызвал код сериализации массива объектов в строку. Всё по фэншую: разные функции обходят с помощью паттерна “визитор” массивы вариантов и формируют нужные строки. Код типобезопасный и короткий.
Функций сериализации четыре: GetAltTextFromIds, GetAltText, RenderInlines, DebugAst, но для примера приведу только одну, которая покороче:
using InlineNode = std::variant<....>;std::pmr::string GetAltText(const std::pmr::vector<InlineNode>& nodes) { std::pmr::string result; for (const auto& node : nodes) { std::visit( [&result, this](auto&& arg) { using T = std::decay_t<decltype(arg)>; if constexpr (std::is_same_v<T, Text>) { result += arg.content; } else if constexpr (std::is_same_v<T, Code>) { result += arg.content; } else if constexpr (std::is_same_v<T, SoftBreak>) { result += ' '; } else if constexpr (std::is_same_v<T, HardBreak>) { result += ' '; } else if constexpr (std::is_same_v<T, Emphasis>) { result += GetAltTextFromIds(arg.children); } else if constexpr (std::is_same_v<T, Strong>) { result += GetAltTextFromIds(arg.children); } else if constexpr (std::is_same_v<T, Link>) { result += GetAltTextFromIds(arg.children); } else if constexpr (std::is_same_v<T, Image>) { result += arg.alt_text; } }, node); } return result;}
Выглядит элегантно. Изначально из-за этого кода я думал назвать статью как-то менее вызывающе. После рассмотрения недостатков я планировал привести этот код как положительный пример. Мол, вот здесь ИИ написал лучше, чем это сделает не очень начитанный C++ программист. Итого: местами не очень получилось, а где-то прям здорово.
Перед написанием хвалебной части мне, естественно, понадобилось посмотреть на код внимательнее и попробовать разобраться, что к чему. Зачем я только это сделал… Остался бы и дальше очарованным паттернами, ключевыми словами constexpr и кодом, красиво оформленным в столбик.
Оказывается, и здесь плохо. Сейчас и вам расскажу почему.
Итак, стоит задача хранить массив неких элементов, которые по-разному сериализуются в строки.
Простым подходом будет создать базовый класс и наследовать от него разные сущности. В контейнере тогда будут храниться указатели на него. А для сериализации понадобится в разных случаях вызывать разные виртуальные функции. Получилась бы приблизительно следующая иерархия классов:
using namespace std::literals;struct Node{ virtual ~Node() = default; virtual std::string StrAltText() = 0; virtual std::string StrDebug() = 0;};struct SoftBreak : public Node { std::string StrAltText() { return " "s; } std::string StrDebug() { return " "s; }};struct HtmlBlock : public Node { std::string content; std::string StrAltText() { return content; } std::string StrDebug() { return std::format("HtmlBlock = {}", content); }};
Массив таких объектов превращался бы в строку как-то так:
std::string GetAltText(const std::vector<Node *>& nodes) { std::string result; for (const auto& node : nodes) result += node->StrAltText(); return result;}
Приведённый код требует ручного управления памятью, да и вообще небезопасен. Ситуацию можно улучшить, храня в векторе умные указатели, например std::unique_ptr. Другой вариант — использовать std::any.
Однако остаётся вторая проблема: код сериализации размазан по множеству классов. Можно собрать обработку в одном месте, узнавая тип объекта в массиве с помощью dynamic_cast. Это медленно.
Поэтому классическим решением будет хранить идентификатор объекта в базовом классе, чтобы быстро определять его тип:
enum NodeType{ kSoftBreak, kHtmlBlock};struct Node{ const NodeType type; Node(NodeType t) : type(t) {} virtual ~Node() = default;};struct SoftBreak : public Node { SoftBreak() : Node(kSoftBreak) {}};struct HtmlBlock : public Node { std::string content; HtmlBlock() : Node(kHtmlBlock) {}};
Теперь обработку, хоть и не очень изящно, можно собрать в одном месте:
std::string GetAltText(const std::vector<std::unique_ptr<Node>>& nodes){ std::string result; for (const auto& node : nodes) switch(node->type) { case kSoftBreak: { // Здесь даже не требуется получать сам объект. // SoftBreak &n = *static_cast<SoftBreak *>(node); result += ' '; break; } case kHtmlBlock: { const HtmlBlock &n = static_cast<const HtmlBlock &>(*node); result += n.content; break; } } return result;}
Если хочется большего изящества, можно воспользоваться типом данных std::variant, что и сделал Claude Opus. Тип std::variant — это такой умный вариант union, безопасный с точки зрения работы типов. Вот только тут есть нюанс. Жирный такой нюанс.
Но начнём мы с того, то ИИ вообще создал странную избыточную сущность. Он завёл перечисление NodeType, которое бывает нужно для схемы, которую мы рассмотрели выше:
enum class NodeType { .... kHtmlBlock, kBlockQuote, kList, kListItem, kText, kSoftBreak, ....};
И вставил бессмысленный числовой идентификатор kType в структуры:
struct SoftBreak { static constexpr NodeType kType = NodeType::kSoftBreak;};struct HtmlBlock { static constexpr NodeType kType = NodeType::kHtmlBlock; std::pmr::string content; int block_type = 0; // CommonMark HTML block type (1-7)};
Это перечисление и идентификатор в структурах не нужны. kType нигде не используется, благо объявлен он как static и не занимает память в каждом объекте.
Само перечисление используется в единственном месте — в функции NodeTypeToString:
// Convert NodeType to string for debugginginline std::string_view NodeTypeToString(NodeType type) { switch (type) { case NodeType::kDocument: return "Document"; case NodeType::kParagraph: return "Paragraph"; case NodeType::kHeading: return "Heading"; case NodeType::kThematicBreak: return "ThematicBreak"; .... case NodeType::kHtmlInline: return "HtmlInline"; } return "Unknown";}
Вот только непонятно, зачем и как этой функцией пользоваться на практике. Зачем она? Чем она поможет? Кажется, никому и ничем. В общем, всё это можно удалить и сократить размер кода проекта ещё строк на 100.
Ну лишний код, и ладно — не привыкать. Дальше нас ждёт самое толстое. В прямом смысле слова.
Особенность переменной типа std::variant в том, что она должна иметь достаточный размер для размещения внутри себя наибольшей альтернативы. В нашем случае вариант должен вмещать в себя следующие классы:
using InlineNode = std::variant<Text, SoftBreak, HardBreak, Code, Emphasis, Strong, Link, Image, HtmlInline>;
Размер этих классов весьма разный, начиная с самых маленьких, таких как SoftBreak:
struct SoftBreak { static constexpr NodeType kType = NodeType::kSoftBreak;};
Размер этого класса 1 байт. Хотя класс пустой, он всё равно должен занимать хотя бы один байт.
Самый большой класс — Image:
struct Image { static constexpr NodeType kType = NodeType::kImage; std::pmr::string destination; std::pmr::string title; std::pmr::string alt_text;};
Его размер в 64-битной программе может составлять 120 байтов. Дело в том, что std::pmr::string, например, в реализации STL занимает 40 байтов. Вы уже поняли, что это означает?
Вариант ещё дополнительно хранит информацию, какой тип он содержит в текущий момент. Итого размер одного объекта типа InlineNode составляет 128 байтов! Можете здесь сами посмотреть.
Итого, программа работает с массивами, где размер каждого элемента 128 байтов. Офигительная “эффективность” расхода памяти.
Столько памяти нужно даже для того, чтобы хранить пустые классы, обозначающие разделители и превращающиеся обычно в пробелы:
} else if constexpr (std::is_same_v<T, SoftBreak>) { result += ' ';
Чтобы знать, где напечатать пробел, требуется хранить 128 байтов.
Перерасход памяти сам по себе, может, не такая уж большая проблема, но здесь ещё возникает и проблема производительности. При работе с таким растянутым массивом менее эффективно будет использоваться и кэш процессора.
Можно возразить, что вариант с умными указателями добавляет уровень косвенности при доступе к объекту, что тоже замедлит работу. И ещё нужно самому тип объекта проверять (см. выше пример со switch).
Не принимается. Косвенный доступ по указателю не существенный, учитывая, что потом всё равно будет куча обращений к памяти для чтения строк и их конкатенации. Перерасход памяти выглядит куда более существенной проблемой. Что по поводу проверки типа объекта, так она всё равно есть, просто скрыта внутри std::variant.
Ещё представьте, как болезненно происходит расширение вектора, если очередной элемент уже не влезает в зарезервированный буфер. Например, здесь:
std::pmr::vector<InlineNode> link_content;for (size_t i = opener->pos + 1; i < result.size(); ++i) { link_content.push_back(std::move(result[i]));}
При расширении вектора происходит перемещение объектов на новые места. Брр… Версия с умными указателями на базовый класс std::vector<std::unique_ptr<Node>> куда легковеснее.
Итог: перед нами вновь говнокод, поданный в красивой обёртке.
Я не говорю, что умные указатели или std::any здесь является лучшим решением. Можно что-то и другое придумать. Если задача — писать эффективный код, то это хорошее место, чтобы подумать, как хранить объекты и обходить их для сериализации.
Что ещё?
Не знаю. Я и так уже вышел за лимит времени, который собирался посвятить просмотру кода и статье.
Это ещё раз говорит о росте ценности ревьюеров. Нагенерировать код всё проще. Вопрос: где взять силы на его просмотр и вынесение вердикта, получилось ли то, что нужно? Кто возьмёт ответственность за результат? Мы всё-таки в мире C++, суть использования которого — эффективность. Если кому-то скорость вообще не нужна, что он тут забыл?
Внимательный разбор завершаю, но напоследок пройдусь по некоторым предупреждениям PVS-Studio.
Склероз
Предупреждение: V560 [CWE-571] A part of conditional expression is always true: !input.empty(). markus.h 2228
explicit LineBuffer(std::string_view input) { if (input.empty()) [[unlikely]] { return; } .... // input не модифицируется .... if (input.size() > line_start || (!input.empty() && (input.back() == '\n' || input.back() == '\r'))) { line_offsets_.push_back({line_start, input.size() - line_start}); } ....}
ИИ педантично вставляет [[unlikely]], чтобы подсказать компилятору, что это маловероятное событие. Но при этом через некоторое время забывает, что далее строка всегда непустая, и влепляет бессмысленную повторную проверку !input.empty().
С кем поведёшься, от того и наберёшься
Предупреждение: V1048 [CWE-1164] The ‘prev_line_had_content’ variable was assigned the same value. markus.h 4090
void ExtractLinkReferences(Document& doc) { .... bool prev_line_had_content = false; .... if (prev_line_had_content) { prev_line_had_content = true; // This line also has content Advance(); continue; } ....}
Переменной prev_line_had_content присваивается значение true, хотя из проверки и без того следует, что там лежит это значение. Анализатор прав — перед нами бессмысленная строчка кода. Но предупреждение неинтересное: я бы не стал его рассматривать, если бы не комментарий.
Слышали байки, что программисты иногда пишут верные, но бесполезные комментарии? Видимо, у ИИ были “хорошие” учителя. Это как раз тот случай, когда комментарий фактически пересказывает выполняемое действие, не добавляя ничего нового.
См. также «Вредный совет N53 — Отвечай в комментарии на вопрос “что?” в книге “60 антипаттернов для С++ программиста”.
Создание временных прокси объектов
Предупреждение: V823 Decreased performance. Object may be created in-place in the ‘result’ container. Consider replacing methods: ‘push_back’ -> ‘emplace_back’. markus.h 2597
std::pmr::vector<InlineNodeId> ParseInlines() { std::pmr::vector<InlineNode> result; .... result.push_back(Text(text_.substr(text_start, pos_ - text_start))); ....}
В начале создаётся временный объект типа Text, затем он помещается в контейнер. Первая мысль: использовать emplace_back:
result.emplace_back(Text(text_.substr(text_start, pos_ - text_start)));
Но изменение на emplace_back в этом случае даст примерно 0% ускорения, так ещё и нагрузит дополнительно компилятор — ему придётся делать инстанцирование шаблона функции emplace_back. Внутри него уже будет идеально передаваться созданный объект, который будет перемещён конструктором перемещения.
Здесь придётся написать более хитрый код, чтобы действительно получилось размещение сразу в контейнере. Это контейнер вариантов, значит, мы должны подсказать ему, какую именно альтернативу собираемся создавать, и передать её std::string_view.
Вот как это будет выглядеть:
result.emplace_back(std::in_place_type<Text>, text.substr(....));
В таком случае мы идеально передадим подсказку о конструируемом типе и аргументы конструктора этого типа. Нас интересует 5-я перегрузка конструктора std::variant.
При таком изменении мы избавимся от одного вызова конструктора перемещения.
По отдельности мелочь, конечно, но если таких вставок много, то почему бы и не написать сразу хорошо
-
V823 Decreased performance. Object may be created in-place in the ‘result’ container. Consider replacing methods: ‘push_back’ -> ‘emplace_back’. markus.h 2610
-
V823 Decreased performance. Object may be created in-place in the ‘result’ container. Consider replacing methods: ‘push_back’ -> ‘emplace_back’. markus.h 2632
-
V823 Decreased performance. Object may be created in-place in the ‘result’ container. Consider replacing methods: ‘push_back’ -> ‘emplace_back’. markus.h 2650
-
V823 Decreased performance. Object may be created in-place in the ‘result’ container. Consider replacing methods: ‘push_back’ -> ‘emplace_back’. markus.h 2733
-
V823 Decreased performance. Object may be created in-place in the ‘result’ container. Consider replacing methods: ‘push_back’ -> ‘emplace_back’. markus.h 2751
-
V823 Decreased performance. Object may be created in-place in the ‘result’ container. Consider replacing methods: ‘push_back’ -> ‘emplace_back’. markus.h 2754
-
V823 Decreased performance. Object may be created in-place in the ‘result’ container. Consider replacing methods: ‘push_back’ -> ‘emplace_back’. markus.h 2855
-
V823 Decreased performance. Object may be created in-place in the ‘result’ container. Consider replacing methods: ‘push_back’ -> ‘emplace_back’. markus.h 2931
-
V823 Decreased performance. Object may be created in-place in the ‘quote_lines’ container. Consider replacing methods: ‘push_back’ -> ‘emplace_back’. markus.h 5239
-
V823 Decreased performance. Object may be created in-place in the ‘quote_lines’ container. Consider replacing methods: ‘push_back’ -> ‘emplace_back’. markus.h 5241
-
V823 Decreased performance. Object may be created in-place in the ‘item_lines’ container. Consider replacing methods: ‘push_back’ -> ‘emplace_back’. markus.h 5641
-
V823 Decreased performance. Object may be created in-place in the ‘item_lines’ container. Consider replacing methods: ‘push_back’ -> ‘emplace_back’. markus.h 5643
-
V823 Decreased performance. Object may be created in-place in the ‘para_lines’ container. Consider replacing methods: ‘push_back’ -> ‘emplace_back’. markus.h 6045
-
V823 Decreased performance. Object may be created in-place in the ‘para_lines’ container. Consider replacing methods: ‘push_back’ -> ‘emplace_back’. markus.h 6047
-
V823 Decreased performance. Object may be created in-place in the ‘delimiter_stack’ container. Consider replacing methods: ‘push_back’ -> ‘emplace_back’. markus.h 2729
-
V823 Decreased performance. Object may be created in-place in the ‘delimiter_stack’ container. Consider replacing methods: ‘push_back’ -> ‘emplace_back’. markus.h 2746
-
V823 Decreased performance. Object may be created in-place in the ‘line_offsets_’ container. Consider replacing methods: ‘push_back’ -> ‘emplace_back’. markus.h 2193
-
V823 Decreased performance. Object may be created in-place in the ‘line_offsets_’ container. Consider replacing methods: ‘push_back’ -> ‘emplace_back’. markus.h 2196
-
V823 Decreased performance. Object may be created in-place in the ‘line_offsets_’ container. Consider replacing methods: ‘push_back’ -> ‘emplace_back’. markus.h 2208
-
V823 Decreased performance. Object may be created in-place in the ‘line_offsets_’ container. Consider replacing methods: ‘push_back’ -> ‘emplace_back’. markus.h 2218
-
V823 Decreased performance. Object may be created in-place in the ‘line_offsets_’ container. Consider replacing methods: ‘push_back’ -> ‘emplace_back’. markus.h 2221
-
V823 Decreased performance. Object may be created in-place in the ‘line_offsets_’ container. Consider replacing methods: ‘push_back’ -> ‘emplace_back’. markus.h 2229
-
V823 Decreased performance. Object may be created in-place in the ‘result’ container. Consider replacing methods: ‘push_back’ -> ‘emplace_back’. markus.h 2637
-
V823 Decreased performance. Object may be created in-place in the ‘result’ container. Consider replacing methods: ‘push_back’ -> ‘emplace_back’. markus.h 2947
-
V823 Decreased performance. Object may be created in-place in the ‘result’ container. Consider replacing methods: ‘push_back’ -> ‘emplace_back’. markus.h 2958
Копия вместо перемещения
Предупреждение: V820 The ‘content’ variable is not used after copying. Copying can be replaced with move/swap for optimization. markus.h 4670
struct Heading { static constexpr NodeType kType = NodeType::kHeading; int level = 1; // 1-6 std::pmr::vector<InlineNodeId> children; std::pmr::string raw_content; // Temporary storage for inline parsing};std::optional<Heading> TryParseAtxHeading() { .... std::pmr::string content(trimmed.substr(i)); .... Heading heading; heading.level = level; heading.raw_content = content; // <= return heading;}
Строка в переменной content копируется, хотя её можно переместить, так как дальше она не нужна. Можно написать:
heading.raw_content = std::move(content);
А можно обойтись вообще без временного объекта heading и написать что-то типа:
return Heading { .level = level, .raw_content = std::move(content) };
Опять-таки, рассмотренное место — мелочь, но постепенно накапливаются лишние действия
-
V820 The ‘info_string’ variable is not used after copying. Copying can be replaced with move/swap for optimization. markus.h 4761
-
V820 The ‘content’ variable is not used after copying. Copying can be replaced with move/swap for optimization. markus.h 4762
-
V820 The ‘content’ variable is not used after copying. Copying can be replaced with move/swap for optimization. markus.h 5101
-
V820 The ‘content’ variable is not used after copying. Copying can be replaced with move/swap for optimization. markus.h 5821
Закат солнца вручную
Предупреждение: V837 The ‘insert’ function does not guarantee that arguments will not be copied or moved if there is no insertion. Consider using the ‘try_emplace’ function. markus.h 5274
using LinkRefMap = std::pmr::unordered_map<std::pmr::string, std::pair<std::pmr::string, std::pmr::string>>;std::optional<BlockQuote> TryParseBlockQuote() { .... LinkRefMap* parent_link_refs_ = nullptr; .... for (const auto& [label, dest_title] : nested_doc.link_references) { if (parent_link_refs_->find(label) == parent_link_refs_->end()) { parent_link_refs_->insert({label, dest_title}); } } ....}
PVS-Studio предупреждает о неэффективности, если в контейнере уже есть элемент с таким же ключом. В нашем случае защита от этого сделана вручную, но она неэффективна. В коде произойдут два поиска:
-
Сначала ищем элемент по ключу для проверки условия.
-
При вставке производим эту же операцию снова, пытаясь найти нужную позицию вставки в хэш-таблицу.
Вызов try_emplace в этой ситуации сделает ровно один поиск и вставит элемент, если его там не было:
for (const auto& [label, dest_title] : nested_doc.link_references) { parent_link_refs_->try_emplace(label, dest_title);}
Заключение
Выводы всё те же, что были в предыдущей статье “Давайте заглянем в этот самый вайб-код”. Независимо от того, как вы создаёте программный код, для обеспечения качества и надёжности разрабатываемого ПО следует использовать комплекс мер.
-
Цените архитекторов и разработчиков, которые умеют декомпозировать и строить модульную архитектуру.
-
Помните, что безопасность закладывается в систему с момента её проектирования (КИБ).
-
Цените разработчиков, которые могут отличить эффективный код от медленного, безопасный от опасного. Тех, кто может разобраться и принять решение, имеет ли право созданный код стать частью проекта.
-
Используйте статические анализаторы кода.
-
Используйте динамические анализаторы кода и FAST анализаторы для frontend-приложений.
-
Используйте инструменты композиционного анализа. В случае использования ИИ это особенно актуально, так как их галлюцинации начали применять для атак на цепочки поставок.
-
Используйте стандарты кодирования. Нет ничего хуже, когда код написан вразнобой.
-
Примените по необходимости другие практики, обобщённо называемые разработкой безопасного ПО.
Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Andrey Karpov. Let’s check vibe code that acts like optimized C++ one but is actually a mess.
ссылка на оригинал статьи https://habr.com/ru/articles/1026084/