Является ли преждевременная оптимизация корнем всех зол

от автора

Среди программистов распространена поговорка: «Преждевременная оптимизация — корень всех зол». Откуда она взялась? В каком контексте использовалась? Насколько все еще применима?

Рассмотрим, в чем опасность преждевременной оптимизации. Есть вообще в ней смысл? Мартейн Фаассен разбирает знаменитое высказывание Дональда Кнута. Подробности под катом.

Мы в Selectel готовим новый сервис. Если арендуете серверы в рабочих или личных проектах, нам очень поможет ваш опыт — записывайтесь на короткое онлайн-интервью. За участие подарим плюшевого Тирекса и бонусы на услуги Selectel.

Дайте ссылку!


Изучение источников — всегда хорошо. Что ж! Рассмотрим оригинальное высказывание повнимательнее.

Рассматриваемая цитата принадлежит Дональду Кнуту и взята из статьи «Структурное программирование с операторами перехода» 1974  года.

Разберем для начала само название. Что означает «структурное программирование»? Его использует каждый, в том числе, я. Ирония в том, что «структурное программирование» доминирует в мире разработки настолько основательно, что нам уже не нужен и термин для его описания.

Однако в конце 60‑х и начале 70‑х широко обсуждалось: стоит ли заменять goto функциями и структурирующими операторами более высокого уровня — такими как, if, for и while. Традиционное на тот момент «неструктурированное» программирование с goto — когда можно было совершать скачок в любое место программы — могло быть более эффективным. Сейчас этот спор звучит странно, но тогда статья Кнута описывала структурное программирование как «революцию».

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

Он отмечает:

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

То же самое могу сказать и про записи в моем блоге. Однако Кнут все-таки смог убедить людей в своих взглядах на оптимизацию. Теперь все повторяют его. Следующее утверждение по-прежнему актуально:

«Другими словами, похоже, фанатичные сторонники «нового программирования» перегибают палку в своем строгом насаждении морали и чистоты в коде».

Программисты различных племен по-прежнему убеждены, что знают истину, и продолжают искать чистоту.

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

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

Оператор безусловного перехода

Тот самый goto начисто исключен из большинства языков. Решительная победа структурного программирования! Но почему? Компиляторы стали лучше? Да, но что еще важнее — компьютеры теперь настолько быстрые, что смешно беспокоиться о накладных расходах структурного программирования — они попросту перестали иметь какое бы то ни было значение. Да и много ли разработчиков скучают по goto?

Вычисления с плавающей точкой

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

Глобальные переменные

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

Семафоры

Я не эксперт в многопоточном программировании, но семафоры — полезный примитив. Современные среды разработки как раз и строятся так, чтобы дать абстракции более высокого уровня. Означает ли это, что семафоры ушли, как и goto?

Указатели

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

Присваивание

Да оно повсюду! Однако всё заметнее движение в сторону чистого функционального программирования, которое выступает против прямого присваивания значений. Опытные разработчики (вне зависимости от своего выбора в языке программирования) тревожатся из‑за ограничении изменяемости в различного рода контекстах. При этом никто не отрицает того, что неизменяемые значения обладают рядом полезных свойств.


Назад к Кнуту


Кнут продолжает:

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

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

Цитата о преждевременной оптимизации


Вернемся к нашей цитате.

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

Давайте рассмотрим высказывание в контексте.

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

Мудрые слова. Читаем дальше.

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

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

Благодаря своей убежденности Кнут выступал за то, чтобы профайлеры встраивались в среду разработки. Да, поддержка профайлеров повсеместна, но она, как правило, не «на виду».

Перечитаем еще раз.

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

Контекст имеет значение! Инструменты, которые Кнут не хочет упускать из виду, включают в себя языковую конструкцию goto.

Как видно, Кнут был человеком, крайне озабоченным производительностью. Он написал целую книгу об эффективности алгоритмов!

Изменившийся контекст


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

Ситуация стала довольно радикально меняться в 1974 году.

Компьютеры того времени были чрезвычайно ограничены в ресурсах по сравнению с устройствами, которые у нас есть сегодня. Производительность систем того времени была настолько ничтожна, что приходилось реализовывать одновременную работу нескольких пользователей с одним на всех процессором. Средний смартфон как минимум в тысячу раз быстрее, чем та ЭВМ, которую Кнуту приходилось делить с коллегами. Даже посудомоечная машина сегодня имеет бо́льшую вычислительную мощность! Неудивительно, что люди были одержимы производительностью. Они были настолько озабочены ей, что беспокоились о последствиях отказа от goto.

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

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

Python в наши дни — один из самых популярных языков программирования в мире. Его доминирующая реализация, CPython — также одна из самых медленных в мире. Сделано это намеренно.

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

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

Помню, 30  лет назад зацикленность на производительности языков программирования встречалась гораздо чаще. Примерно в 1990 году я выучил ассемблера Z80 только потому, что BASIC был недостаточно быстрым. Еще несколько лет — и я выучил C (он чуть медленее ассемблера, зато более высокого уровня). Частично причина моего выбора заключалась в той самой озабоченности производительностью. Я был неопытным разработчиком, а доступные в то время компьютеры на порядки уступали в вычислительной мощности тем, что у нас есть сейчас.

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

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

Производительность как маркетинговый трюк


Вы можете возразить: «Мы же видим бенчмарки различных фреймворков! Значит разработчики по‑настоящему заботятся о скорости!» Возможно.

Не исключено, что такая забота о производительности фреймворков объясняется простотой ее реализации. Мы имеем дело с маркетинговым трюком. Например, говорят, что веб-фреймворк для Python быстрее. Я и сам так считал. Однако давайте не будем забывать, что и сам фреймворк написан на Python. Если кому действительно нужна безумно высокая производительность при одноядерной низкоуровневой обработке HTTP, то я бы посоветовал тому взглянуть на другие языки.

Так или иначе, когда я последний раз задавался этим вопросом и проводил тесты, Flask был одним из самых популярных веб-фреймворков Python. При этом — одним из самых медленных в тривиальных задачах запросов‑ответов. На самом же деле это почти никого не волнует! Это не важно практически для любого варианта использования. Скорость его работы приемлема — и достаточно.

Реклама рассказывает, насколько быстро очередной JavaScript Frontend Framework обновляет DOM и насколько мал полученный код. В редких случаях это важно — например, когда скорость загрузки страницы критична. Для большинства же веб-приложений размер кода совершенно несущественен и не идет ни в какое сравнение с накладными расходами на загрузку изображений. Затратность обновления дерева DOM попросту тонет в других факторах.

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

Почему оптимизация может быть злом


Для ответа на этот вопрос возвращаемся Кнуту:

  • растрачивается время программиста,
  • оказывается сильное негативное влияние на отладку и поддержку.

Спасибо, Кнут!

Когда оптимизация преждевременна


Когда приводит ко злу — потере времени или усложнению поддержки кода (что опять‑таки приводит к потере времени).

Преждевременная оптимизация в малом


Часто ли в наши дни оптимизируют код?

Да, особенно в библиотеках с открытым исходным кодом. Многие из нас (разработчиков — прим. пер.) время от времени тратят немного усилий на ускорение чего‑то ради удовольствия от ремесла. Чаще всего — в коде библиотек и фреймворков.

Расходует ли это наше время? Иногда, но не обязательно. Мы можем упражняться в свободное время. В какой‑то момент постигнем несколько трюков, которые однажды выручат в по‑настоящему в затруднительном положении. Существенное улучшение производительности может даже привести к вышеупомянутому скачку качества.

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

Вывод: некоторые из усилий по оптимизации могут быть преждевременными, но не все и не всегда.

Как насчет микрооптимизаций в коде приложения? Видим ли мы разработчиков, скажем, заменяющих одну конструкцию цикла for в JavaScript на другую, быструю, но при этом более громоздкую? Уверен, такое происходит. Проблема ли это для большинства команд? Не думаю, хотя оговорюсь, что работал с исключительно сбалансированными разработчиками.

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

Преждевременная оптимизация в большом


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

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

С таким видом преждевременной оптимизации мы сталкиваемся довольно часто. Растрачивается время программиста, усложняется поддержка кода — именно поэтому мы вправе объявить подобную практику злом. Кнут же говорил о микрооптимизациях, таких как goto‑переходы.

Часто оптимизация не является злом


Часто оптимизация просто прекрасна! Можно же объединить лучшее из обоих миров!

Представим, что нужно найти что-то в хэш-таблице, скажем, в словаре Python. Я мог бы написать так:

def lookup(d, lookup_key):   for key in d.keys():       if key == lookup_key:           return d[key]   return None 

Мы ничего не оптимизировали пока. Производительность в порядке, если словарь небольшой. Оптимизированная версия выглядит так:

def lookup(d, lookup_key):    return d.get(lookup_key) 

Здесь я использую метод словарей Python get, который находит элементы за постоянное время, независимо от размера словаря.

Вот цикл for гораздо опаснее: его и проследить сложнее, и поддержка затрагивается. К тому же он медленнее и не так хорошо масштабируется.

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

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

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

Однажды я настаивал на использовании указателя в кодовой базе. У нас был Python’овский for для фильтрации записей. Мы знали, что фильтр будет востребован. Также понимали, что однажды записей может стать много. Мне говорили, что я не должен вводить указатель, если я не докажу с помощью профилирования медлительность цикла for. Профилирование, особенно с реальными данными, потребовало бы усилий. Затраты же по внедрению указателя были сравнительно небольшими и не затрудняли поддержку. Мне казалось, что компромиссы были ясны.

Мудрость из мудрости


Возвращаемся к статье Кнута. Ему пишет Дейкстра и рассуждает о славе gotoGo To Statement Considered Harmful. И далее он говорит буквально следующее: «С нетерпением жду того дня, когда машины станут настолько быстрыми, что нам не придется больше заниматься оптимизацией».

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

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

А как же мудрость утверждения «Преждевременная оптимизация — корень всех зол»? Да, она все еще актуальна в некоторых особенных случаях — которые, как правило, противоположны низкоуровневым оптимизациям, о которых говорил Кнут.

И все же, возможно, нам стоит возродить в нашем сообществе старую добрую склонность к оптимизации. А что думаете вы? Поделитесь мнением в комментариях!


ссылка на оригинал статьи https://habr.com/ru/articles/892748/