Предварительная оптимизация — корень всех зол?

gtvdcgm62n_kmz9dydiovihynii.jpeg

Эта статья — реакция на другую статью Производительность главнее всего. Да, в интернете опять кто-то не прав. Однако я хочу несколько выйти за рамки обсуждаемой темы, чтобы дополнить и углубить рассмотрение важных аспектов.


Оригинальная статья Кнута

Конечно, Кнут не нуждается в том, чтобы его кто-то защищал. В самом деле, кто такой Кнут, а кто такой я? Однако моя цель — не защита Кнута, а защита здравого смысла.

Оказывается, что контекст статьи очень важен. Поэтому приведу ссылку, чтобы читатель мог самостоятельно ознакомиться с оригиналом статьи: http://cowboyprogramming.com/files/p261-knuth.pdf

Итак, перед нами статья 1974 года Стенфордского Университета под названием «Структурное программирование с оператором GOTO» (здесь и далее перевод авторский). Меня еще не было, а статья уже была. Я подозреваю, как и многих из вас, читающих статью.

Как нетрудно догадаться из названия, статья про структурное программирование и про то, как написать чистый и эффективный код без использования GOTO. Сейчас это кажется самоочевидным. Однако не в 1974. Это была одна из прорывных статей, где утверждалось крайне неочевидное для того времени утверждение: программы без GOTO можно писать эффективным образом.

Тем не менее, рассмотренные примеры говорили, что GOTO дает выигрыш. Программа без GOTO становится более простой, однако цена — это скорость исполнения.

Рассмотрим примеры из статьи:

Пример 2.

A[m+1] := x; i := 1;
while A[i] != x do i := i + 1;
if i > m then m := i; B[i] := 1;
else B[i] := B[i] + 1 fi;

Пример 2a.

A[m+1] := x; i := 1; go to test;
loop:  i := i + 2;
test:  if A[i] = x then go to found fi;
       if A[i+1] != x then go to loop fi;
       i := i + 1;
found: if i > m then m := i; B[i] := 1;
       else B[i] := B[i] + 1 fi;

Пример 2а дает прирост производительности примерно на 12%, т.е. является более быстрой реализацией. Однако он и более сложен.


Заметка на полях. Современные компиляторы позволяют производить такого рода оптимизации без переписывания кода. Это еще один камень в огород преждевременной оптимизации.

Итак, ручное манипулирование циклами дает прирост производительности. Однако Кнут задается вопросом:, а стоит ли оно того?

Теперь приведу полную цитату:


There is no doubt that the grail of efficiency leads to abuse. Programmers waste enormous amounts of time thinking about, or worrying about, the speed of noncritical parts of their programs, and these attempts at efficiency actually have a strong negative impact when debugging and maintenance are considered. We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil.

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

Т.е. известная цитата — это даже не предложение, а лишь его часть. И внезапно, с учётом контекста, всё становится гораздо более осмысленным и понятным:


  1. Мы не знаем, какие участки кода действительно являются критичными.
  2. Тратя огромное время на микрооптимизации мы упускаем другие важные аспекты программы: поддержка и отладка.
  3. Простота на начальном этапе написания кода важнее скорости, если речь идет про микрооптимизации.

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

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


Современные системы или «что делать?»

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

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

Однако возникает резонный вопрос:, а когда имеет смысл задумываться о производительности?

У меня есть ответ на этот нелегкий вопрос: в самом начале и в самом конце! И да, в середине тоже можно, при наличии тестов и бенчмарков.

Итак, мы подходим к вопросу масштабирования и производительности.


Масштабирование

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

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


  1. Какими навыками и компетенциями обладает твоя команда? Смогут ли они легко использовать фреймворк?
  2. Как поддерживается фреймворк? Какой релизный цикл? Быстро ли отвечают на вопросы и дефекты?
  3. Каким качеством обладает фреймворк? Есть ли положительный опыт у команды/компании.
  4. Легко ли интегрировать в инфраструктуру компании?
  5. Какова лицензия?
  6. И т.п.

Все эти вопросы — про предсказание и ответы на вопрос: «а что будет если?».

То же самое относится и к масштабированию и производительности. Разные продукты и проекты имеют разные требования к производительности и масштабируемости. Более того, жизненный цикл продукта может потребовать иметь запас в производительности.

Тем не менее, иногда важно быстро выпустить что-то, чем люди бы пользовались, нежели выпустить масштабируемое приложение, которое никому не нужно. Часто это баланс производительности инженеров, и производительность бывает достаточна даже для PHP/Python, чтобы завоевать весь мир. Я думаю, нет смысла приводить здесь конкретные примеры.


Архитектура

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

Таким образом, стоит констатировать следующий факт: производительность — это часть архитектурного решения.

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

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


Аспекты распределенных систем

Современные распределенные системы являются сложными многоаспектными продуктами. При этом масштабируемость и производительность — не единственные факторы, на которые стоит обращать внимание.

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

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

Мы приходим к следующему уровню понимания: есть приоритеты в архитектурных областях. И приоритет масштабируемости и производительности не стоит на первом месте.

На этом всё.

© Habrahabr.ru