Чистый код работает медленно, но он все равно нужен

Если вы еще не в курсе, то приготовьтесь: в Twitter в области программирования/разработки ПО происходит нечто интересное. Все началось с одного из видеороликов Casey Muratori (Кейси Муратори) о производительности программирования. Ролик называется “Clean Code, Horrible Performance” (“Чистый код, ужасная производительность”). Кейси критикует современное ПО, эффективность которого менее 1/20. Это обоснованное беспокойство, поскольку неэффективность является причиной того, что мы не можем получить качественные продукты. Затем в разговор вмешивается Uncle Bob (Дядя Боб), т. е. Robert C. Martin (Роберт К. Мартин), который провел с Кейси очень детальное и познавательное обсуждение с использованием GitHub. Дискуссия продолжается и по сей день. К тому же она породила тему в Hacker News.

Чистый код действительно медленный

Я посмотрел видео и считаю, что Кейси, хотя и прав, не совсем точно обрисовал картину в целом. Он утверждает: полиморфизм и принцип единственной ответственности (если ему слепо следовать)  —  это тормоза (а значит, зло), что в какой-то мере верно. К примеру, полиморфизм чреват затратами на поиск в координирующей таблице и перенаправления. Не говоря уже о том, что функция и/или данные будут разбросаны, а не находиться рядом друг с другом, что приведет к пропускам нужных данных в кэше CPU L1/L2. То же самое касается и принципа единственной ответственности. Распаковка функции на несколько частных вспомогательных функций также может привести к пропуску данных в кэше. К этому же приводят и встраиваемые функции. К сожалению, полиморфизм является основой SOLID  —  5 ключевых принципов чистого кода:

  • SRP  —  принципа разделения обязанностей/единственной ответственности;
  • OCP  —  принципа открытости/закрытости;
  • LSP  —  принципа подстановки Лисков;
  • ISP  —  принципа разделения интерфейса;
  • DIP  —  принципа инверсии зависимости.

Производительность действительно важна

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

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

Существует множество способов сделать код производительным.

  1. Выбор подходящего алгоритма. Это, пожалуй, самое простое. От выбора алгоритма зависит, будет ли код работать как Flash или как Eeyore. Так что обновляйте свои алгоритмы.
  2. Использование соответствующего оборудования. Например, кэшей L1/L2 и CPU для ускорения работы кода. Кэш L1 может работать в 100 раз быстрее, чем оперативная память, а L2  —  в 25 раз. Но для этого вам необходимо знать, как функция и данные расположены в памяти: чтобы кэш работал, они должны располагаться близко друг к другу. Для этого можно использовать паттерны Data-Oriented Design и Entity-Component-System.
  3. Дополнительные наборы инструкций для CPU, такие как MMX, SSE и AVX.

Но чистый код тоже важен

Однако есть и другие пути, кроме достижения только производительности. Я видел много примеров плохо написанного и нечитаемого кода. И большинство из них  —  из моего опыта работы разработчиком игр. Разработчики игр печально известны тем, что стремятся к производительному коду. Потому что это позволяет добиваться большего контентного наполнения, качества, количества трисов, текстур и т. д., чтобы игра выглядела лучше без ущерба для FPS.

Но я считаю, что если производительность становится приоритетом, то в итоге можно получить такой же глючный конечный продукт, как движок Source Engine. Печально известный дефект “coconut.jpg”, вероятно, является результатом нарушения принципа единственной ответственности. Если вы изменяете одну часть кода, это может повлиять на другую часть, которая не имеет никакого отношения к той, над которой вы работаете. Это произошло у меня с Blackberry Messenger. В BBM iOS есть класс BBMessage. Нам запрещено трогать этот класс без особой необходимости. А если все же приходится работать с ним, то нужно быть осторожным, потому что это может привести к поломке других вещей.

Высокопроизводительный код не самый простой для чтения и сопровождения. Если такой вам попадется, вы в этом убедитесь. Попробуйте, к примеру, понять суть кода, созданного на алгоритме Fast Inverse Square Root для видеоигры Quake. Хотя не весь этот код непонятен, некоторые места в нем  —  сплошной мрак.

Чистый код и другие современные принципы программирования в большей степени ориентированы на процесс разработки, а не на производительность. Они используются для сокращения времени программирования, а не для повышения производительности. Их цель  —  сделать код более читаемым, тестируемым, надежным, расширяемым и сопровождаемым. Иногда (или чаще всего) за счет производительности. Причина в том, что процессор и память обходятся дешевле, чем время разработки и сопровождения. Поэтому рекомендуется добавлять мощность процессора и объем памяти, чтобы компенсировать недостаток эффективности, создаваемый чистым кодом.

Должен сказать, что когда я переквалифицировался из разработчика игр в разработчика мобильных приложений (в основном для iOS) в KMK Labs/Vidio.com, то испытал настоящий шок. Парадигма требований в новом офисе оказалась совершенно другой. Мне было довольно трудно адаптироваться к приоритету чистоты и сопровождаемости кода над производительностью. Скорость процесса разработки мобильных приложений важнее, чем конечный продукт (хотя и в приемлемом смысле). Поэтому мне пришлось изучать чистый код, SOLID, чистую архитектуру, TDD, MVVM, экстремальное программирование и т. д.

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

Так что же делать?

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

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

  1. Овладейте основами разработки. Не нужно глубоко проникать в то, что происходит внутри компьютера. Вам достаточно понимать, что происходит “под капотом” настолько, насколько это необходимо для работы с кодом. Освойте также на базовом уровне используемые вами язык программирования и инструменты.
  2. Изучите домен. Под доменом в программной инженерии обычно понимается предметная область, на которую рассчитано приложение. Глубокое знание домена позволяет понять, что именно пытается решить тот или иной код. Только в этом случае можно написать код, наиболее соответствующий потребностям домена. Например, для домена приложения “Очередь в больнице” не нужна производительность, измеряемая в наносекундах. Поэтому можно сократить время разработки. Иначе обстоит дело с видеоиграми, где требуется рендеринг в реальном времени. Но даже в этом случае некоторые части кода нуждаются в тщательной проработке. Возможно, рендеринг в реальном времени номера в очереди для приложения “Очередь в больницу” потребует повышения производительности. Или для игры более высокого уровня понадобится максимально чистый код, который облегчит создание модов.
  3. Используйте абстракции грамотно. Имея глубокие знания о домене, вы можете создать стратегию подхода к проблеме. После этого необходимо использовать абстракции по решению проблемы. Абстракции можно определить как сокрытие сложной и ненужной для данного контекста информации и придание ей большей значимости. В данном случае абстракция  —  это не просто абстрагирование, как в объектно-ориентированном программировании. Это лишь один из видов абстракции. Типы переменных  —  это абстракции. Они абстрагируют биты, составляющие реальное значение в памяти. Именование переменной  —  это абстракция. Это абстрагирование неизвестной переменной в нечто более релевантное текущему контексту переменной. Операции и функции являются абстракциями регистровых инструкций. Именование операции или функции  —  это абстракция. Структуры данных  —  это абстракции. Слои  —  это абстракции.
  4. Будьте прагматичны. В конце концов, цель программирования  —  предоставить пользователю решение в виде приложения. Разумеется, вы должны дать пользователю то, что он хочет получить от приложений или игр. Возможно, ему нужна производительная игра с более чем 100 FPS? А может, он заинтересован в приложении с большим количеством функций, надежном, без ошибок и поставляемом как можно быстрее? Вы должны быть прагматичным, располагая теми инструментами, которые у вас есть, потому что пользователям все равно, как вы выполните свою работу. Знаете крылатое выражение про молоток и гвозди? Если у вас в руках молоток, то вам везде мерещатся незабитые гвозди. Не надо так делать.
  5. Проявляйте гибкость. Поймите, что парадигмы, паттерны и принципы программирования  —  это лишь инструменты и ориентиры, позволяющие усовершенствовать код. Они не являются жесткими правилами, которые необходимо выполнять всегда и любой ценой. Не каждый switch-case надо изменять в соответствии с принципом открытости/закрытости. Не каждый объект нужно преобразовывать из “массива объектов” в “объект массивов” в соответствии с DOD (Data-Oriented Design, решение, ориентированное на поток данных). Любые правила нужно применять с учетом конкретной ситуации. А чтобы понять, какое из них следует использовать, необходимо глубокое понимание основ разработки и области применения.
  6. Больше заботьтесь о коде и команде. Эта рекомендация, пожалуй, не требует пояснений. Плохой код (т. е. медленный и/или грязный) возникает у программиста, который не заботится о нем. Заботьтесь о своем коде, товарищах по команде, продукте и пользователях. Только тогда вы станете производить максимально качественный продукт. В конце концов, вы ведь пишете код не для одного себя.

Заключение

Напоследок приведу цитату Роберта К. Мартина (он же Дядя Боб), взятой из его беседы с Кейси Муратори:

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

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Bawenang Rukmoko Pardian Putra: Clean Code Is Slow, but You Need It Anyway…

Предыдущая статьяПочему разрабатывать веб-интерфейсы так сложно?
Следующая статьяКак Snowflake повышает эффективность dbt-моделей на Python