Мы используем куки, чтобы пользоваться сайтом было удобно.
Хорошо
to the top

Вебинар: Go-Go-Gadg...Error? Смотрим, как ошибаются Go разработчики! - 26.08

>
>
>
Опухший C++ код

Опухший C++ код

19 Авг 2026

Есть такая старая программистская байка, что нельзя платить программистам за строки кода, так как тогда они будут писать длинный бестолковый код и любить метод 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, но уже вырисовывается картина новых бед, и как инструмент сможет помочь с ними справиться.

Дополнительные ссылки:

Подписаться на рассылку
Хотите раз в месяц получать от нас подборку вышедших в этот период самых интересных статей и новостей? Подписывайтесь!
Популярные статьи по теме

Комментарии (0)

Следующие комментарии next comments
close comment form