МИФ: "Чистый код по канонам Clean Code = быстрый код"
Логика соблазнительная: раз "хороший код" хорош по всем осям, то полиморфизм вместо switch, маленькие функции и сокрытие внутренностей делают систему заодно и быстрой.
Но в горячих циклах, которые крутятся миллионы раз, эта интуиция ломается: там всё решает физика процессора.
❌ Миф
Следуй правилам Clean Code – полиморфизм вместо ветвлений, прячь внутренности объектов, дроби на маленькие функции – и получишь код одновременно поддерживаемый и производительный. "Чистота" ничего не стоит во время исполнения.
✅ Реальность
Правила Clean Code, которые влияют на структуру исполняемого кода, системно душат производительность на горячих циклах.
В феврале 2023 Кейси Муратори (Handmade Hero) взял пример из самой Clean Code-литературы – иерархию фигур с виртуальным Area() – и замерил суммирование площадей массива:
🔹 полиморфизм ("чистая" версия): ~35 циклов на фигуру;
🔹 switch + плоская структура: ~24 цикла, в 1.5 раза быстрее;
🔹 табличная диспетчеризация (table-driven): 3.0-3.5 цикла, ~10x.
Формулировка Муратори: отказ от полиморфизма стирает 3-4 года эволюции железа, а table-driven – уже 12-14 лет.
📖 Откуда взялся этот миф
Clean Code Роберта Мартина стала догмой индустрии, а "polymorphism > switch всегда" – её символом веры.
Правило родилось в мире, где узким местом считались вычисления. Но главное узкое место современного процессора – память: оперативная память в сотнях циклов от исполнительных блоков, промах кеша стоит дорого.
Учебники ООП считают, что лишний уровень косвенности (индирекция) бесплатен. На деле за него платит железо – промахами кеша.
🔍 Доказательства
1️⃣ Массив указателей на полиморфные объекты ломает аппаратный предзагрузчик (prefetcher): промах кеша почти на каждый элемент, а указатель на таблицу виртуальных функций (vptr) раздувает объект и вытесняет полезные данные из строки кеша.
2️⃣ Главный ущерб – невозможность встроить вызов (заинлайнить). Компилятор не видит тело виртуальной функции и теряет оптимизации: свёртку констант, разворачивание циклов. В бенчмарке japreiss конкретная версия развернулась в цикл (2.86 цикла), виртуальная осталась на 11.52 – разница 4x.
3️⃣ Науку подтверждает Data-Oriented Design (проектирование вокруг данных): Wingqvist et al. (IEEE GEM 2022) – до 13.25x на игровой симуляции за счёт непрерывных в памяти данных вместо объектов (SoA против AoS).
💡 Что делать на самом деле
Обратное "чистое = всегда медленно" тоже ложь.
Значительную часть замедления снимает компилятор (final, LTO, CRTP, std::variant), а упор в процессор (CPU-bound) в бизнес-софте редко оказывается реальным ограничением: 99% кода живёт на <1% процессора, и там когнитивная нагрузка важнее сэкономленных тактов.
Практика: раздели систему на модули по требованиям к задержке.
🔸 Для обычной логики – Clean Code религиозно.
🔸 Для горячего участка, который выявил профилировщик, – раскладка данных в памяти (data layout), SoA, статическая диспетчеризация.
🔸 Не меняй полиморфизм на switch рефлекторно: сначала попробуй final/LTO/CRTP. И профилируй перед оптимизацией: смотри perf stat и P99, не среднее.
Проектируй как собор, оптимизируй как механик на свалке – но только в моторном отсеке. Знать, где что, и есть инженерная компетенция.
📚 Что почитать:
Casey Muratori – "Clean" Code, Horrible Performance (computerenhance.com) – первоисточник со всеми замерами.
Переписка Muratori ↔️ Uncle Bob (github.com/unclebob/cmuratori-discussion) – образец инженерного спора о том, что оптимизировать.