понедельник, 13 февраля 2017 г.

Про преждевременную оптимизацию

Большинство программистов считает, что преждевременная оптимизация  зло. Они рассуждают в следующем ключе: для бизнеса важнее написать работающую программу и побыстрее, поэтому пишем неоптимизированный код, а потом, если будет тормозить, то возьмём профайлер и оптимизируем горячие места.
Мне кажется, в этих рассуждениях упущен один важный момент: когда вы начнёте оптимизировать программу, может случиться так, что вы не найдёте очевидных мест просадки производительности (или после оптимизации программа будет всё равно работать медленно). Просто неэффективный код будет размазан по всему проекту.
Допустим, рендеринг страницы состоит из последовательного вызова 30 функций. Тогда общее время рендеринга будет равно (t1 + Δt1) + (t2 + Δt2) + ... (t30 + Δt30), где ti  время выполнения оптимизированной функции, а ti + Δti – время выполнения неоптимизированной функции. Тогда просадка производительности будет равна Δt1 + Δt2 + ... + Δt30. Благо, если вы найдёте большие Δti, виновные в тормозах приложения. А если все Δti приблизительно одного порядка? Тогда придётся анализировать весь код, что может быть очень затратно. Одно дело – сразу оптимизировать код во время написания, другое – оптимизировать этот же код спустя пару месяцев, когда все детали успешно забыты.
Так что же делать? Однозначного ответа на этот вопрос нет. Я стараюсь поступать следующим образом. При написании программы я оптимизирую код, если:
  • переписанный код не потеряет в надёжности, безопасности и читаемости по сравнению с оригинальным неоптизированным кодом,
  • переписывание кода не займёт у меня слишком много времени,
  • я точно уверен, что переписывание действительно улучшит быстродействие приложения.
Например, если я знаю, что в ArrayList'е всегда будет лежать как минимум несколько тысяч элементов, то я заменяю вызов дефолтного конструктора ArrayList'а на конструктор с указанием initialCapacity. Такая замена тривиальна и нисколько не ухудшает читаемость кода, зато может сэкономить пару микросекунд времени выполнения алгоритма.
Или, например, HashMap<Integer, Integer> я заменяю на TIntIntMap из trove4j. В этом случае безопасность даже повышается, т.к. у TIntIntMap сигнатуры более строгие, чем у Map, и не позволяют, к примеру, написать map.containsKey("abc").
Самое трудное – это выполнить третий пункт. Дело в том, что очень часто трудно понять, имеет ли вообще смысл оптимизация, ведь современные компиляторы сложны и непредсказуемы. И поэтому в кодах программ встречается огромное количество бессмысленных оптимизаций, в результате чего в среде программистов и сложилось мнение, что вообще все преждевременные оптимизации зло. Но ведь не все оптимизации бессмысленны, верно?