В индустрии разработки игр (а может софта и вообще) живёт очень удобная идея, которой многие оправдывают лишние слои логики и абстракции. Звучит она примерно так: «Я хочу, чтобы сам процесс создания игр был проще, приятнее и продуктивнее и готов пожертвовать 10% производительности ради этого» - Тим Суини.

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

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

Статья родилась после очередного приступа клинокодомании в отдельно взятой студии, когда кто-то в очередной раз увидел и притащил всем известную книгу Мартина о достоинствах этой самой клинокодии. Но "Clean Code", тот что с заглавной буквы и тысячными тиражами, вовсе не «чистый код» в бытовом смысле, потому что Мартин придумал этот бренд, ярлык, и несколько лет пестовал его на конференциях, связав свое имя и это понятие. Правильнее было бы называть это "стиль мистера Мартина" или "стиль Дядюшки Боба", и тогда половина споров о важности книги отваливается сама собой, потому что спорить приходится уже не про абстрактную "чистоту", а про конкретные наборы приёмов конкретного автора. Та дискуссия с технической стороны так и не получила внятных результатов, потому что на третьем круге обсуждения в ход шла риторика и подмена понятий вроде «clean» (хороший, годный), который начал превращаться в «Clean» (по методичке), и обратно.

Не все "Ёгурты" одинаково полезны

Тут нужна небольшая поправка к тексту статьи и почему не стоит бросаться использовать примеры из красной книги у себя в проекте. Когда Мартин писал Clean Code (2008), он уже много лет занимался не только разработкой, но и консультированием команд через Object Mentor, которая обучала экстремальному программированию, объектно-ориентированным подходам и agile-идеям. Поэтому книга выросла из опыта работы с большими бизнес-системами и командами, где главной проблемой обычно была сложность изменения и поддержки кода, а не стоимость каждого микросекундного участка выполнения.

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

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

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

Возьмем что-то совсем приземленное, обход массива объектов, к примеру и попробуем посчитать сумму какого-то поля. «Чистый» вариант почти всегда выглядит невинно, вектор shared_ptr, и виртуальный метод, удобная иерархия, или граф типов, но проблема начинается не там, где код становится чистым. Проблема начинается там, где абстракция становится целью, а не инструментом.

// "удобно", но каждый объект это отдельная аллокация
for (Entity* e : scene.getAllEntities())
    e->getTransform()->update(dt);   // виртуальный вызов + виртуальный вызов 
                                     // + прыжок по куче

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

Второй вариант, тот что без иерархий и интерфейсов, держит просто массив структур (или, ещё лучше, массив отдельных полей, structure of arrays) и плоский цикл, который компилятор разворачивает и векторизует сам, без подсказок. Этот вариант "грязнее" в смысле Мартина, сложнее читать новичку и хуже укладывается в красивую иерархию классов из учебника, но его проще профилировать, потому что в нем нечему прятаться, проще распараллелить, потому что данные лежат рядом, и он в разы быстрее на железе. Это не жертва ради скорости, это просто другой способ думать о данных, не как об объектах с поведением, а как о массивах данных, которые надо быстро прогнать через процессор, коими это все и было, пока мы не попробовали это все дело приукрасить и "почистить".

// плотный проход по массиву: и читать приятно, и кеше хорошо
for (size_t i = 0; i < count; ++i)
    positions[i] += velocities[i] * dt;   // SoA, предсказуемый доступ

И вот когда мы приходим к измерению fps, замеров расхода памяти или профилированию на реальном железе, то серьёзных сторонников «Clean Code» становится в половину меньше, а все заявления о том, что мы меняем циклы CPU на продуктивность программиста, так и остаются заявлениями.

Но предположим, мы действительно оказались в ситуации, где приходится менять производительность на скорость написания кода. Допустим даже, что у нас есть инструменты, про которые точно известно, что они так делают, но даже в этом идеальном для них случае выбирать комфорт программиста почти всегда неразумно. Главная ошибка, которая ломает весь довод «за скорость написания в ущерб скорости выполнения» это построение неправильной экосистемы с самого начала.

Я тебя написал, я тебя и запущу

Неявная модель, на которой все это держится выглядит так, что у нас есть один производитель (который пишет код) и один потребитель (которые его запускает). Производитель страдает над клавиатурой, потребитель наслаждается результатом. Красиво! Только в разработке игр это не так от слова совсем.

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

ИТЕРАЦИЯ РАЗРАБОТЧИКА
  правка кода / ассета
  компиляция (плохая архитектура -> +2 минуты на инкрементальный билд)
  запуск редактора / cooking ассетов
  прожать до нужной сцены
  посмотреть результат 10 секунд
  -> заметить что не то
  -> повторить N раз

И вот тут плохое время исполнения бьёт по продуктивности самого автора и часть «выигрыша» от того, что код написали "чище", тут же съедается ожиданием. Пока соберётся проект, пока прогрузится уровень, пока ты дойдёшь до бага на десятой минуте геймплея, потому что быстрого сейва в этой точке нет, а загрузка занимает полминуты, пока... пока... пока...

Чтобы и дальше оправдывать подходы, ускоряющие генерацию плохого кода, нужно доказать, что ухудшение цикла итераций не перевешивает выигрыш в скорости написания. А ведь код пишется один раз, а запускается, отлаживается и подкручивается сотни и тысячи раз и каждая лишняя секунда компиляции и каждый просевший до 20 fps редактор умножаются на это число. Подумайте про компиляцию своего проекта, вы где-то добавили <iostream> , он добавил вам почти !секунду! на компиляции, вы это код собираете по 60 раз за день, минута времени просто ушла в никуда, а это просто один заголовочный файл, но их в проекте сотни, и каждый со своим сайдэффектом. Эта секунда критична (объясню ниже), но этого никто не считает, пока время не начинает переваливать за минуты, десятки минут или часы. Часы ожидания.

Тормоза распространяются

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

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

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

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

Тормоза накапливаются

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

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

Цифра реального активного проекта, художник в среднем за день делает 40 генераций ассетов, не рисует заново, но что-то меняет и обновляет/cмотрит в игре, я взял только импорт ассета без запуска редактора и просмотра на реальном уровне.

быстрый импорт ассета: 0.2 c   ->   медленный: 8 c
                                     × 40 генераций ассетов в день
                                     × 40 художников
                                     = +4 часа ожидания в день

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

«Почему редактор тормозит?», да потому что плагин дёргает менеджер, который под капотом на каждый тик аллоцирует строки, потому что так было «чище» написать три года назад в библиотеке, которую с тех пор никто не открывал, и это, кстати, отлично объясняет состояние современных игр и инструментов.

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

Тормоза тормозят всех

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

16.6 мс на кадр @ 60 fps
  - 4.0 мс  рендер
  - 3.0 мс  физика
  - 2.0 мс  анимация
  - 5.6 мс  геймплей-логика
  --------
  = 2.0 мс  запас

// "красивый" ECS-обёрточный слой добавил по 0.0008 мс на сущность
// × 30 000 сущностей = 2.4 мс -> запас в минусе -> кадр застолился

Возьмем рендер на 60 fps, это 16 миллисекунд на кадр всего, и в этот бюджет нужно уложить физику, анимацию, AI, рендер и звук разом. Если где-то в недрах системы частиц лежит «чистая» абстракция с виртуальным вызовом на каждую частицу, то при десяти тысячах частиц на сцене это уже полмиллисекунды, три процента от всего бюджета кадра, потраченные не на частицы, а на красоту кода. Один такой класс не уронит игру, но в реальном проекте таких "чистых" решений сотни, и просадка набегает не из одного места, а из тысячи маленьких, каждое из которых по отдельности оправдано читаемостью.

Жертвы чистого кода

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

Я начал с цитаты Суини про 10% производительности ради удобства программиста, но беда в том, что разработка игр сегодня сплошь и рядом оказывается сложнее, муторнее и менее продуктивной потому, что система научилась оправдывать посредственность красивыми словами. Чистый код пожирает сначала скорость ради удобства разработчика, а в потом забирает и и скорость проекта и удобство разработки.

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

З.Ы. Тут тоже ругаются на чистый код и Тут

З.З.Ы. Еще я завел сайт на github pages, где собрал часть статей, которые относятся к разработке и играм (https://dalerank.github.io/). Статьи переведены на английский и частично итальянский, за что отдельное спасибо коллегам и нашему отделу локализации.