Есть такая старая программистская байка, что нельзя платить программистам за строки кода, так как тогда они будут писать длинный бестолковый код и любить метод copy-paste. Будущее наступило. Только теперь этими “программистами” является генеративный ИИ (GenAI), которому как раз платят за строки кода. Иронично.

Один из моих интересов — изучение сгенерированного С++ кода, чтобы понимать, как развивается индустрия создания ПО, какие проблемы уходят, а какие наоборот возникают. После заметки “Дайте посмотреть на нормальный С++ проект, созданный вайб-кодингом” мне предложили заглянуть в проект VibeTensor, что я и сделал.
VibeTensor: System Software for Deep Learning, Fully Generated by AI Agents
Я проверил его с помощью статического анализатора PVS-Studio, а также посмотрел С++ код глазами. Было интересно узнать, как много ошибок в нём можно найти с помощью классического обзора кода и статического анализа.
Так вот, у меня нет ответа на этот вопрос. Непонятно, потому что главная проблема этого кода в том, что он ужасно раздут. Это сильно мешает его обзору. Мне тяжело продираться сквозь это болото, а вместе со мной “вязнет” и статический анализатор.
Впрочем, ожидать большого количества ошибок здесь тоже не стоит: по-настоящему полезного кода в этом проекте кот наплакал.Как же так? Проект вроде не такой уж маленький. Проанализированных C++ файлов более 400, а количество строк кода — около 100,000.
Да, но это только кажется, что там есть что смотреть и анализировать. Повторюсь: основная проблема качества этого кода — его избыточность. Она выражена как в постоянном повторении блоков кода, так и просто в бессмысленных лишних действиях, растягивающих код.
Раньше бы сказали, что этот проект писался методом copy-paste. В данном случае это не так, но генерация кода приводит ровно к таким же последствиям. Вместо выноса обобщённой функциональности в функции, вновь и вновь генерируется код для решения схожим проблем.
Можете полистать файлы, и через некоторое время вас начнёт преследовать дежавю, что вы вновь и вновь видите одни и те же блоки кода. Они вроде как и разные, а вроде как и нет. Вот что я имею в виду:

Например, я уже писал в статье “C++: Пиши, сокращай, оптимизируй”, что этот блок кода можно встретить 9 раз в разных тестах:
const std::size_t nd = sizes.size(); std::vector<int64_t> strides(nd, 0); int64_t acc = 1; for (std::ptrdiff_t i = static_cast<std::ptrdiff_t>(nd) - 1; i >= 0; --i) { strides[static_cast<std::size_t>(i)] = acc; const auto sz = sizes[static_cast<std::size_t>(i)]; acc *= (sz == 0 ? 1 : sz); } int64_t ne = 1; bool any_zero = false; for (auto s : sizes) { if (s == 0) { any_zero = true; break; } ne *= s; } if (any_zero) { ne = 0; }
Однако это ещё не всё. PVS-Studio сыплет предупреждениями про постоянную избыточность. Иногда это касается мелочей:
for (int i = 0; i < dl.ndim; ++i) { int64_t n = (dl.ndim == 0) ? 1 : dl.shape[i]; int64_t d = n > 0 ? (n - 1) : 0; if (d == 0) continue; int64_t st = (dl.ndim == 0) ? 1 : strides[static_cast<std::size_t>(i)];
PVS-Studio дважды выдаёт V547 Expression ‘dl.ndim == 0’ is always false. Действительно, если цикл выполняется, то dl.ndim не может быть равен нулю. Код упрощается до:
for (int i = 0; i < dl.ndim; ++i) { int64_t d = std::max(0ll, dl.shape[i] - 1); if (d == 0) continue; int64_t st = strides[i];
В других местах пухлость кода мелочью уже не назовёшь. Там PVS-Studio выдаёт сразу группы предупреждений:
V547 [CWE-570] Expression ‘is_empty’ is always false. tensor_bindings.cc 3007
V547 [CWE-570] Expression ‘print_size’ is always false. tensor_bindings.cc 3015
V547 [CWE-571] Expression ‘!parts.empty()’ is always true. tensor_bindings.cc 3023
Код, на который выданы предупреждения, на первый взгляд умный, с массивом, с циклом... А если присмотреться — лабуда.
bool is_empty = false; // handled above; always false here bool print_size = is_empty && (self.sizes().size() != 1); bool suppress_dtype_non_empty = (!is_empty) && (self.dtype() == ScalarType::Float32 || self.dtype() == ScalarType::Int64 || self.dtype() == ScalarType::Bool); bool print_dtype = !suppress_dtype_non_empty; if (is_empty) { // For empty tensors, only print dtype when dtype != default float32 print_dtype = (self.dtype() != ScalarType::Float32); } std::string out = "tensor("; out += body; std::vector<std::string> parts; if (print_size) { parts.push_back(std::string("size=") + format_sizes(self.sizes())); } if (print_dtype) { parts.push_back(std::string("dtype=") + dtype_name(self.dtype())); } // Always include device suffix for CUDA tensors parts.push_back(std::string("device='cuda:") + std::to_string((int)self.device().index) + "'"); if (!parts.empty()) { out += ", "; for (std::size_t i = 0; i < parts.size(); ++i) { if (i) out += ", "; out += parts[i]; } } out += ")"; return out;
Как минимум, ручной цикл формирования сообщения можно сразу заменить на:
return std::format("tensor({})", parts | std::views::join_with(", "sv));
Если присмотреться получше, то вообще всю эту избыточную фиговину можно сократить в три раза:
std::string out = "tensor(" + body + ", "; if (self.dtype() != ScalarType::Float32 && self.dtype() != ScalarType::Int64 && self.dtype() != ScalarType::Bool) { out += std::string("dtype=") + dtype_name(self.dtype()) + ", "; } out += "device='cuda:" + std::to_string((int)self.device().index) + "')"; return out;
Вот что здесь интересно: если рассуждать об анализе исходного варианта, то вроде как ошибок не находится. Код сложный, правильно работает, выхода за границу массива нет. Почтение к ИИ.
Когда же код схлопывается до своей сути, то понимаешь, что там и ошибаться то негде. Он просто написан длиннее. Не к чему тут относиться с почтением.
Итого: нет в проекте никаких настоящих 100,000 строк С++ кода. Думаю, что если вынести дубликаты в функции и провести рефакторинг, количество кода сократится раз в 5. Проект на 20,000 строк кода — это баловство. Вот и вижу в нём не ошибки, а проблему раздутого кода и предупреждения анализатора про большое количество ложных/истинных условий и т.п.
Ну получается код длиннее, и что? Он и не предназначен для рефакторинга человеком. Если надо — новый сгенерируем.
Если хочется продать GenAI, а на судьбу проекта всё равно, то ради бога. Если же вам нужен проект, то “плата за строки кода” куда выше, чем кажется.
Следствия раздутого кода:
Больше строк кода — больше плата за их генерацию.
Если Pull Requests ревьювит другой ИИ, то и ему больше плати.
Раздутые функции — дороже генерация юнит-тестов.
Любая модификация кода с помощью ИИ дороже, так как требуется больше строк кода принять и отдать.
Много контекста — больше вероятность ошибок при внесении изменений (например, можно просто что-то не исправить в одном из 100500 похожих мест).
Если человеку самому придётся править код или искать баг — у него вытекут глаза. Очень тяжело продираться сквозь нагромождение избыточных сущностей и конструкций.
Когда код сложнее, чем нужно, компилятор будет хуже его оптимизировать.
Проще сгенерировать ещё одну функцию, похожую на другие, чем найти и переработать уже существующие десятки однотипных функций.
Лишние конструкции мешают не только человеку, но и статическому анализатору искать ошибки.
Код медленнее компилируется.
Излишнее “словоблудие” увеличивает вероятность столкнуться с неопределённым поведением, или что код будет работать не так, как задумывалось.
Можете сами продолжить список.
Под пунктом №11 я имел в виду, что если не понимаешь смысл слов, то не надо их использовать для красоты. Использованный GenAI не знает суть noexcept, но считает, что с ним “красивее”. Результат — множество мест в коде, где кидается исключение там, где его быть не должно:
vt_status vt_tensor_iter_binary_cpu_host(const vt_iter_config* cfg, vt_tensor out_h, vt_tensor a_h, vt_tensor b_h, vt_tensor_iter_loop1d_fn loop, void* user_ctx) noexcept { .... if (effective.check_mem_overlap != VT_ITER_OVERLAP_DISABLE && effective.check_mem_overlap != VT_ITER_OVERLAP_ENABLE) { throw std::invalid_argument( "vt_tensor_iter_binary_cpu: invalid vt_iter_overlap_mode"); } .... }
При этом проблема разбухшего кода — это ваша проблема, а не продавцов ИИ. Вам платить за токены.
Что можно сделать? Готовых решений предложить не могу. Но, по крайней мере, предупреждён — значит вооружён.
Я же всё больше склоняюсь к мнению, что нужно развить в статическом анализаторе PVS-Studio направление по выявлению схожих фрагментов кода. Тогда можно будет замкнуть GenAI и PVS-Studio в петлю обратной связи. Тогда код будет считается доделанным, если анализатор не только молчит про баги, но и нет попыток дублирования функциональности.
Пока это ещё не роадмап развития PVS-Studio, но уже вырисовывается картина новых бед, и как инструмент сможет помочь с ними справиться.
Дополнительные ссылки:
Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Andrey Karpov. Bloated C++ code.

