Обновить

Ревью вайб-кода с гнильцой, который притворяется оптимизированным С++ кодом

Уровень сложностиСредний
Время на прочтение24 мин
Охват и читатели31K
Всего голосов 94: ↑92 и ↓2+110
Комментарии47

Комментарии 47

Я не строг, а требователен. Для реализации проекта взят язык С++. Суть использования C++ — построение быстрых, эффективных приложений, экономно использующих память. Если вам не нужна быстрая программа, не надо использовать C++.

C++ вполне приятный язык. Его можно использовать и для когда, для которого не важны оптимизации. Его метапрограммирование многого стоит, да ещё и со статической типизацией. Много ещё языков есть, которые позволяют такое?

Выглядит элегантно. Изначально из-за этого кода я думал назвать статью как-то менее вызывающе. После рассмотрения недостатков я планировал привести этот код как положительный пример. Мол, вот здесь ИИ написал лучше, чем это сделает не очень начитанный C++ программист. Итого: местами не очень получилось, а где-то прям здорово.

Не выглядит оно элегантно. Если человек не способен распознать тут проблемы прямо сразу, то это не начитанный C++ программист. Тут сразу видна проблема в том, что создают промежуточные строки, а потом сразу же копируются в общую. Для сериализации в строки есть прекрасный апи через std::formatter. Внутри же можно было бы вызывать std::format_to и не использовать промежуточные строки.

Ещё уродливый if-chain с кучей if constexpr. Зачем? Ведь существует std::visit и перегрузки (можно было бы через концепты сократить код).

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

Далеко не лучшее решение. Намного удобнее было бы взять std::variant из std::unique_ptr. Не будет проблемы перерасхода памяти, будет элегантный апи std::variant, можно будет использовать std::visit и перегрузки с концептами. Так ещё и std::variant работает быстрее виртуальных функций (у меня в одной статье освещалось то, что компиляторы их лучше оптимизируют. Да и бенчмарки самого visit найти можно). Промежуточные std::string тем временем никуда не пропали.

Там, где речь шла про SIMD, не помешал бы ассемблер в статье. Не помешали бы и бенчмарки при сравнении варианта с std::unique_ptr<Node> и оригинального std::variant.

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

Ну там не просто if-ы, а этапа компиляции (if constexpr). Я к тому, что это работает не так, как вы возможно подумали :)

ИИ часто такое выдаёт. Если выдаёт, то должно использоваться и очень много. Даже когда ему явно прописано, что так делать нельзя. Но пугаться тут нечего - тут просто не самый лучший код.

Нечто подобное было бы получше:

void foo(auto& a) requires requires {...} { 
  return ...; // Тут концепты нужны для определения перегрузки.
              // С ними не нужно прописывать полный набор типов руками.
              // Но можно и прописать: можно задать перегрузки с типами,
              // а можно и в копцепте сделать std::same_as<T, V1> 
              // || std::same_as<T, V2> || ...
}

void foo(auto& a) {
  return ...;
}

std::visit([&](auto& value) {
  return foo(value);
}, v);

Или через overloaded делать перегрузки.

template <typename... Fs>
class Overloaded : public Fs... {
 public:
  using Fs::operator()...;
};

std::visit(Overloaded{
  [&](auto& a) requires requires {...} {
    return ...;
  },
  [&](auto& a) {
    return ...;
  }
}, v);

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

Есть и средний вариант - выражать всё через функции и перегрузки, а не if-chain. Тогда неправильным перегрузкам будет сложно вызваться (остаются только неявные преобразования), а код будет более элегантным уж точно.

Чем ваш вариант с перегрузками лучше?

Тем что нужно помнить правила overload resolution и считать какие из концептов более ограничены?

Тем что нужно накидывать больше символов для идентичного функционала?

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

Для такого случая правила overload resolution с концептами тривиальны. С концептами там меньше повторений кода будет.

Больше символов? else if constexpr(std::is_same_v<T, V1>) {} против ,[&](V1& v){}. И это в случае явного перечисления через перегрузки. Если с концептами, то там сильно меньше получается.

Вариант оригинальный (про него же речь?) то справляется. Но элегантным его точно не назовёшь. Про проблемы варианта через енам сказано.

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

А не займет ли это в итоге больше времени, чем написание кода руками?

Но для этого надо уметь писать код руками. А как известно, для C++ обучение этому навыку занимает как минимум 21 день.

А какой язык можно выучить за время меньшее чем 21 день, да и так это очень маленький срок, для плюсов то тем более

Никто уже не помнит классику.

Вы ошиблись с картинкой, исправляю

C++ in 21 day
alt
C++ in 21 day by time-travel meme

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

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

А тесты кто будет ревьюрить? А есть уверенность что тесты не будут такого же качества, как и сам код? У меня был опыт работы в проекте, где используется ии-автоген тесты, и я могу сказать что в таких ни пасс, ни фейлур - не являются показателями работоспособности

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

Не хватает в таких статьях сравнения в конце.

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

Вы можете сами это легко сделать. Проект состоит из одного файла. Скормите его нейронке и попросите её подробно написать, что не так. Потом расскажите :) Я делал такое, глянул на выхлоп и закрыл. Мне такой фигнёй (сравнением) заниматься не интересно.

А пробовали рефакторинг запустить через клод код на Opus в режиме максимального мышления? Почему все забывают, что они еще и рефакторить умеют?
А если Codex последний запустить на рефакторинг (он его дулает лучше опуса)?
Такой тест проводили?

Пожалуйста, проведите такой тест и напишите об этом.

Интересная позиция.
Писать что ИИ пишет плохо не сделав рефакторинг с ним же. Еще минус поставить на комментарий. Гениально. Ничего другого от бОльшей части хабровчан и не ожидаешь.

Нормальная позиция. У меня вклад 430 статей на Хабре, у вас 0. Набросать вопросов дело не хитрое...

Ах ты жук. На заслуги переходишь :)

Типичная логика хабровчанина. Не ляпните такое на linkedin/medium/threads, засмеют.

Раз история продолжается, я и здесь продублирую позицию по таким комментариям.

Это бессмысленная дискуссия, в которой я много раз участвовал, когда ещё никакого ИИ не было.

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

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

И, самое главное, это исследование не имеет практического смысла. Да, в мире есть разные подходы к поиску и устранению ошибок. Я показал конкретные, реально существующие ошибки, найденные методом статического анализа кода. Неважно, что есть теоретически. Практически — вот ошибки, а значит, полезно использовать PVS-Studio. В конце концов, если просто найти баги другими способами, так чего же они лежали в коде и ждали, пока я приду?

С ИИ схожая ситуация. Я должен идти и рефакторить чужой проект, чтобы в нём исчезли ошибки? Потом вновь исследовать, писать статью... Чтобы доказать что? Теоретическую возможность выправить код с помощью ИИ? Ну, наверное, можно напрячься. Но на практике-то мы всё равно имеем то, что имеем.

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

P.S. Я симметрично груб в ответе набросу, что будто я обязан тестировать рефакторинг чужого проекта разными способами.

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

Кстати, попадалось руководство по троллингу в этих ваших интернетах, там этот прием вполне описан, как один из.

Сделал, может кому интересно будет. Такой незамысловатый промпт:

https://github.com/ryanmcdermott/markus

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

Запускал в последней платной версии GPT 5.4 с функцией Deep Research. Он думал минут 5-10, пока я пил чай и делал другие задачи.
После этого я попросил пройтись по коду и выделить таблицу всех проблемных точек в коде через GPT 5.4 с функцией Thinking

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

DingoBite/markus_repo_ai_errors_find

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

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

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

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

Забыл в статье ещё вот на этот пост сослаться, который мне понравился и который транслирует схожие мысли:

...

Главный риск ИИ-кода — не в том, что он плохой, содержит баги, уязвимости и т.п. В том-то и беда, что он выглядит убедительно хорошим. Агент выдаёт что-то, что компилируется, проходит тесты, рендерит красивый UI. И возникает непреодолимый соблазн пустить это в прод, не заглядывая внутрь.

...

Оххх

автор размышляет о высоких материях, ругая тупой ИИ за избыточность

Видел бы автор, что я навайбкодил на Python, Go, Dart, пытаясь построить корпоративный портал с кроссплафторменным приложением🤣

Несмотря на то что проект работает, хоть им и пользуется 100 человек, если бы автор это увидел - его бы удар хватил😅

Так что автор прав - интересные времена нас ждут.

Материал интересный, но:

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

Подъезжая к сией станцыи и глядя на природу в окно, у меня слетела шляпа.

Ещё давно в детском журнале было, даже с карикатурой: "ученик шёл к доске, исправляя двойку" :)

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

бриф

Скрытый текст
// =====================================================================
// =====================================================================
// мини бриф замороженной разработки
// либа для окна онли и контекста с вводом
// через wrapper.h код не совсем на расте, но прокинут в раст
// #include <X11/Xlib.h> окно
// #include <X11/Xutil.h> расширение
// #include <GL/glx.h> контекст гл
// #include <GL/gl.h> чутка базовых функций типо 1ой версии драйвера
// следующая библиотека
// открыте картинки и сохранение
// код на расте
// следующая библиотека
// glapi или по выбору или загрузка того что нужно под проект
// подгрузка через код раста
// следующая библиотека
// математика
// тут просто будет код на расте
// ------------------------------------------
// итого библиотеки:
// Window, gl, open_img, math3d
// статус:
// Window, gl - сделано внедрено в рамках одной библиотеки
// open_img - сделано не внедрено будет отдельная библиотека(пока думаю как будет)
// math3d - сделано не внедрено будет отдельная библиотека
// =====================================================================
// =====================================================================

теперь ревью и он не плохой впринципе он почти всё уловил

Скрытый текст
Принято, бриф зафиксировал. У тебя получается классический «low-level» стек для графического движка или тулзы на Rust с сильным уклоном в минимализм и прямой контроль над X11/OpenGL.
Вот как выглядит структура твоего проекта на текущий момент:

    Window & GL (Core): Гибридная либа. Сишные потроха (Xlib, GLX) проброшены через wrapper.h, обеспечивая создание окна и контекста. Это фундамент.
    open_img: Готовый модуль на чистом Rust. Ждет решения: делать его зависимостью основного ядра или оставить внешним плагином/утилитой.
    math3d: Чистый Rust. Математика — штука независимая, легко выносится в отдельный крейт.

Пара мыслей по «разморозке» и внедрению:

    По open_img: Если планируешь грузить текстуры прямо «на лету» в GL-контекст, логично сделать его опциональной зависимостью (feature) для основной либы. Если просто работа с данными — лучше оставить отдельным крейтом, чтобы не раздувать бинарник там, где картинки не нужны.
    По math3d: Поскольку ты используешь старый GL (версии 1.x судя по gl.h), математика на стороне Rust будет критически важна для генерации матриц (MVP), так как фиксированный конвейер (fixed-function pipeline) лучше сразу заменять на шейдеры, если glapi позволит.

и пример треугольника тут логика подхода интересная мне понравилось

Скрытый текст
...
    // ===========================================================
    // ===========================================================
    // Компиляция Vertex шейдера
    let vs = gl.create_shader_vertex();//нет ветвлений я доволен
    gl.create_shader_source(vs, 1, VERT_SRC, std::ptr::null());
    gl.compile_shader(vs);

    // Компиляция Fragment шейдера
    let fs = gl.create_shader_fragment();//нет ветвлений я доволен
    gl.create_shader_source(fs, 1, FRAG_SRC, std::ptr::null());
    gl.compile_shader(fs);

    // Линовка программы
    let program = gl.create_program();
    gl.attach_shader(program, vs);
    gl.attach_shader(program, fs);
    gl.link_program(program);
    gl.use_program(program);

    // 3. Данные треугольника (VBO/VAO)
    let vertices: [f32; 9] = [-0.5, -0.5, 0.0, 0.5, -0.5, 0.0, 0.0, 0.5, 0.0];

    let (mut vao, mut vbo) = (0, 0);
    gl.gen_vertex_arrays(1, &mut vao);
    gl.gen_buffers(1, &mut vbo);

    gl.bind_vertex_array(vao);
    gl.bind_buffer_array(vbo);

    gl.bind_buffer_data_array_static_draw(//нет ветвлений я доволен
        (vertices.len() * 4) as isize,
        vertices.as_ptr() as *const _,
    );
    //нет ветвлений я доволен
    gl.vertex_attrib_pointer_float(0, 3, 0, (3 * 4) as i32, std::ptr::null());
    gl.enable_vertex_attrib_array(0);
    // =============================================================================
    // =============================================================================
...
//упор на то что кода будет много, но не будет ветвлений по возможности

по-сути ничего не сделал, просто постучался на иксы(всё банально просто использовал библиотеку X11 ) и это работает, помню на С/С++ мучался с открытием окна...) и прописал как есть 1 раз, в библиотеке всмысле, как итог код чище в самом проекте, всё основное будет разнесено по библиотекам, ну и как следующий итог зависимостей нету, разве что нужны библиотеки враппера, которые описаны в брифе. Всё остальное чистый раст - красота. Окно открывается на lto+--release+lvl-optimisation со скоростью С на данный момент.

Тут смысл в том, что нарисовать треугольник на OpenGL — задача, разжёванная в сотнях статей и книг. От нейросетки тут не требуется изобрести ничего нетривиального, а только «вспомнить», чему её учили. Тем более, кода там совсем немного.

Ещё непонятно, в чём удивление от линейности кода отрисовки треугольника. Ветвлений там даже в максимально наивном коде никто не ожидает.

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

Смысл вложенного в том, чтобы вынести условный выход из цикла во внешний. Только так условные переходы с векторизацией и делаются, раз в 8/16/32 элементов, а не для каждого элемента. В коде Дипсика это же и написано, только другими словами. И ассемблер с парой правок (|| -> |, 8 -> 16) практически такой же: https://gcc.godbolt.org/z/T5hvszb9z

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

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

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

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

Дошли руки протестировать: https://gcc.godbolt.org/z/xqb9GosTe (в godbolt время выполнения нестабильно, но можно потыкать Rerun снизу окна Executor чтоб у лучшего варианта было примерно 0.220, или собирайте локально)

Модифицированный Клод (IsSpanBlankMod) уступает интринсикам, но обгоняет всё остальное. С -O2 вместо -O3 обгоняет всё, но это странно, не буду записывать такое в заслуги.

Интересно было бы посмотреть ваш финальный тест, как там в Win64 получилась разница 8 раз между лучшим и худшим результатом (Win32 даже не комментирую). Видимо, тест жёстко заточен под SIMD и/или компилятор проваливает все варианты, где уже не оптимизировано за него вручную.

Мем с uncanny смешной ;]

Ну ваще энумы в стринги расшифровывать бывает нужно

У меня странный опыт вайбкодинга с C++. Как будто бы AI на плюсовом коде быстро теряет логику происходящего. Я решал одну и ту же задачу( декодирование видео через v4l2 и отображение через DRM) на C#, C++ и Rust. В общем, нужно правильно всё инициализировать, а потом только жонглировать буферами. В C# самым сложным было заставить AI писать правильно p/invoke. Rust - без проблем. C++ - агент как будто бы забывал о структуре проекта, а при начале новой сессии ему было сложно понять код. Может AI плохо понимает конкретно плюсы. Может код на плюсах сам по себе труднопонятен.

AI не умеет понимать - он генерирует текстовые шаблоны (высокой повторяемости) на которых его учили. Как следствие отличие работы с разными языками, я думаю, вызвано теми материалами, на которых проводилось обучение LLM. Попробуйте C++ на другой модели, желательно заточенной под написание кода.

Все делалось на claude sonnet 4.x. Куда уж лучше. Ну и даже не знаю, как ответить на ваш тезис про “не умеет понимать”. Если AI в состоянии разобраться в том, что конкретно делает код и найти логическую ошибку, значит он в каком-то смысле понимает.

Его размер в 64-битной программе может составлять 120 байтов. Дело в том, что std::pmr::string, например, в реализации STL занимает 40 байтов. Вы уже поняли, что это означает?

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

все уже придумано, и без всяких Вариантов:

Design Patterns - Elements of Reusable Object Oriented Software - GOF

Flyweight

Intent: Use sharing to support large numbers of fine-grained objects efficiently. ...

Вайбкод это как тот советский истребитель из анекдота:

- Мы как ни собирали, получался трактор!

- Ну так в инструкции же написано: перед сборкой обработать напильником!

Блин, забавно, что там было "после сборки", а у нас "до сборки (билда)".

Ответное ревью PVS-studio от чата гпт будет?

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Информация

Сайт
pvs-studio.ru
Дата регистрации
Дата основания
2008
Численность
51–100 человек
Местоположение
Россия
Представитель
Андрей Карпов