суббота, 1 ноября 2014 г.

Про "упреждающую оптимизацию"

http://www.gunsmoker.ru/2013/01/optimization-basics.html

"

Неправильная оптимизация

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

Это - близорукий подход. Не только я, но и многие известные эксперты (Стив МакКоннелл, Дональд Кнут, Гарольд Абельсон, Энтони Хоар, Джеральд Сассман, Гордон Белл и др.) говорят примерно одно и то же: преждевременная оптимизация — это корень всех бед. "Программы пишутся для компьютера" - это заблуждение. Программы в первую очередь пишутся для людей, которые будут их читать. Иными словами, писать код нужно в первую очередь для людей, а лишь во вторую очередь - для машины. Именно поэтому простота и ясность лучше сложности и эффективности. К оптимизации нужно обращаться, когда в этом возникает необходимость, не нужно делать это заранее. Если в программе нет проблем с производительностью, то ваша упреждающая оптимизация - это впустую потраченное время и (часто) более запутанный и сложный код. Фактически, вы ухудшаете свою программу - путём её (ненужного и излишнего) усложнения и увеличения времени разработки.

Примечание: передача параметров по ссылке, использование оператора Inc и case и другие аналогичные вещи, которые при работе должны естественным образом "стекать с кончиков пальцев", быть, так сказать, на уровне рефлексов, преждевременной оптимизацией не являются.

Более того, иногда при проведении оптимизации программист допускает ошибки, и корректный, но неэффективный код становится эффективным и... некорректным. Вам может показаться, что можно просто исправить ошибку, но реальность такова, что гораздо проще сделать корректную программу быстрой, чем быструю — корректной. Вот ещё одна причина, чтобы в первую очередь писать простой, удобочитаемый и очевидный код. Такой код проще понять, проще проверить его корректность, проще переделать - и, в том числе, проще оптимизировать. Усложнения же (включая оптимизацию) всегда можно внести позже - и только при необходимости.

Когда нужно начинать оптимизацию?

Итак, вывод: оптимизацию надо начинать...
  1. Когда закончена разработка программы, и написан ясный и корректный код.
  2. Когда есть объективная потребность в оптимизации ("медленно работает, а нельзя ли быстрее?").
    Подсказка: причина "этот код можно улучшить" не является объективной потребностью в оптимизации.
Заметьте, что это также отвечает на вопрос, когда не нужно проводить оптимизацию.

Примечание: сказанное не отменяет необходимости учитывать масштабируемость на реальные данные ещё во время проектирования. Это не относится к "преждевременной оптимизации". 

С чего начать?

Итак, выше я сказал, с чего не надо начинать - не нужно оптимизировать всё подряд, причём до того, как вы выясните, что это нуждается в оптимизации. Оптимизировать одноразовые операции — это просто потеря времени. Вывод? Оптимизировать надо то, что тормозит. Это означает, что начинать вам нужно с определения того, что у вас тормозит. Медленная работа? Высокие потребляемые ресурсы? Ещё чем-то недовольны? Ага, вот это будем оптимизировать. Нет проблем? Всё, не трогаем, не надо ничего оптимизировать.

Таким образом, вам не нужно оптимизировать весь код подряд. Нужно оптимизировать "узкие места" (англ. bottleneck - бутылочное горлышко): критические части кода, которые являются основным потребителем необходимого ресурса. К примеру, вы можете оптимизировать какой-то участок кода, ускорив его выполнение в 1000 раз. Хорошо, теперь ваша программа выполняется на 1 миллисекунду быстрее (потому что до изменений код выполняется за 1 микросекунду). Поздравляю, вы потратили месяц работы, чтобы ваша программа вместо 60 секунд выполнялась за 60 секунд минус одну миллисекунду. Вместо этого вы могли бы потратить пять минут, чтобы выяснить, какой же код работает эти 60 секунд и оптимизировать его. Вы можете потратить всего несколько часов, чтобы ускорить программу в несколько раз. Иными словами, изменение лишь малой части кода приведёт к значительным изменениям. Изменение же любого другого кода не окажет практически никакого эффекта на эффективность. 

Как определить, какой код является узким местом? Следующая ошибка начинающих - пытаться это угадать. "Ну, вот здесь я делаю вот так, это явно ужасно медленно, поэтому вот он, плохой код, это же очевидно". Неверно! Почти никогда вы не угадаете правильно. Сколько я ни занимался оптимизацией своих программ, где-то в 80% случаев я даже близко не был прав в своих предположениях о том, что вызывает тормоза. Вы, наверное, подумали о том, что я, видимо, просто не очень опытен в вопросах оптимизации. Это вполне может быть так (я не буду спорить), но дело ведь не в этом. Подумайте вот о чём: сколько раз вы использовали отладчик, чтобы найти причину бага в программе? Вы использовали инструмент, чтобы разобраться в вашем ясном и предположительно корректном коде. Вы не смогли угадать, в чём проблема с выполнением кода - вам потребовался для этого помощник-инструмент. А если вы не смогли выполнить "в уме" такую простую операцию (проверить корректность кода), то почему же вы считаете, что можете сделать "в уме" гораздо более сложную вещь - разобраться, почему корректный код работает медленно (что включает в себя не только знание кода, как для отладки, но и знание временных характеристик кода, железа, а также внешнего окружения)?

Существует две причины, почему это сложнее, чем простая отладка. Как правило прикладной программист крайне слабо представляет:
  • Как исходный код будет преобразован компилятором в машинный, какие приёмы будет использовать компилятор.
  • Архитектуру современных процессоров - с несколькими работающими параллельно процессорами, глубокой иерархией кэширования, предсказанием ветвления, конвейеризацией и многим-многим другим.
Из-за этого даже "очевидные" для прикладного программиста вещи могут иметь даже противоположный эффект! К примеру, в два раза больший код может выполняться вдвое быстрее.

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

Посмотрите на это и с такой стороны: если вы хотите улучшить производительность, то вы же должны как-то определять, чего вам удалось достичь. Иными словами, на сколько вы улучшили (или, быть может, ухудшили) производительность программы своими изменениями? Оптимизирование - это улучшение характеристик программы. А если у вас на руках нет фактических данных о программе, то чем вы, вообще-то, занимаетесь? Уж явно не оптимизацией..."

ПОВТОРЮ - "А если у вас на руках нет фактических данных о программе, то чем вы, вообще-то, занимаетесь? Уж явно не оптимизацией...".

ПОВТОРЮ - не оптимизацией, а "угадыванием".

ПОВТОРЮ - "не думайте про проблемы которых нет".

Не занимайтесь "преждевременной оптимизацией" или "построением коня в вакууме".

Пример - вот - Рефакторинг. Преодоление "алгоритма маляра".

Там - "нечего оптимизировать". Во втором участке кода.

Потому что статистика прогонов тестов говори о том, что кеш не нужен.

Если кеш был НЕ нужен предыдущие15-ть лет, то может он будет и не нужен и ещё 15-ть СЛЕДУЮЩИХ лет.

И ещё ссылки:

KISS (принцип)
YAGNI
Бритва Оккама

О чём я?

Да ни о чём..

Хотя нет..

Если вы думаете, что код:

      while (l_BlockIndex <> f_Block.Index) do
      begin
       l_Next := f_Block.CreateNext;
       try
        l_Next.SetRefTo(f_Block);
       finally
        FreeAndNil(l_Next);
       end;//try..finally
      end;//l_BlockIndex <> f_Block.Index

Должен выглядеть как:

      while (l_BlockIndex <> f_Block.Index) do
      begin
       l_Next := f_Block.CreateNextCached;
       try
        l_Next.SetRefTo(f_Block);
       finally
        FreeAndNil(l_Next);
       end;//try..finally
      end;//l_BlockIndex <> f_Block.Index

CreateNext => CreateNextCached.

То! Мне с вами - "не по пути".

Ибо есть два момента:

1. CreateNext - и так - фабрика - она может кешировать.
2. Прикладному программисту не надо знать про кешируемость. Более того - это вредно для него. Думы о "преждевременной оптимизации" уводят его мысли от "реализации прецедентов".

И ещё:

3. Если прикладной программист задумывается о кешировании то он либо Geek, либо вы что-то недоделали.

Про "экономию на спичках" читаем тут - Ссылка. Организация памяти в текстовом редакторе

Ну и про "накладные расходы" на создание/уничтожение объекта я могу написать, если интересно.

Обычно - их можно избежать.

Тот же FastMem - их практически нивелирует.

И повторю - начинать оптимизировать можно "тогда и только тогда", когда вы провели НЕСКОЛЬКО часов (а то и суток):

1. С секундомером.
2. За AQTime.
3. За изучением логов куда пишется время (более менее атомарных операций).

3 комментария:

  1. Кстати, я вот в некоторых местах в наших библиотеках сознательно ушёл от использования кэшей. Когда переводил на юникод. Код получился в разы понятнее и надёжнее, а на производительность "на глаз" не повлияло.
    В планах ещё в некоторых местах "порезать" кэши, в частности при доступе к реестру Windows (там, кстати, есть ещё проблема валидации кэша).

    ОтветитьУдалить