Pull to refresh

Насколько плохим код должен быть?

Reading time 6 min
Views 73K
Original author: Eric Lippert
Эрик Липперт — ветеран Microsoft, проработавший в компании 16 лет и стоящий за разработкой VBScript, JScript и C#.

На прошлой неделе в комментариях к одной из статей разгорелся спор о роли низкоуровневой оптимизации в программировании, и я вспомнил относящуюся к этому статью Эрика. Она была написана в конце 2003, и хотя реалии с тех пор несколько изменились — принципы остались теми же самыми. Можете мысленно заменить ASP и VBScript на PHP, JavaScript, или на другой скриптовый язык по вашему вкусу.

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


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

Например, за семь лет в Microsoft я получил десятки вопросов, аналогичных по своей сути этому, заданному в конце 1990-х:
У нас есть код на VBScript, и в одной часто вызываемой функции мы определяем оператором Dim несколько переменных, которые нигде в функции не используются. Не замедляется ли каждый вызов функции из-за объявления этих переменных?
Какой интересный вопрос! В компилируемом языке, таком как Си, объявление локальных переменных общим размером n байт всего лишь вычитает n из указателя стека при входе в функцию. Если n будет чуть больше или чуть меньше, затраты времени на вычитание никак не изменятся. Наверное, в VBScript точно так же? Оказалось, что нет! Вот что я написал автору вопроса:

Никчёмный анализ №1

Объявил переменную — получай переменную. Откуда VBScript может знать, не собирается ли функция выполнить что-то вроде
Function foo()
    Dim bar
    Execute("bar = 123")

Чтобы такой код выполнялся корректно, движок VBScript вынужден во время выполнения хранить список имён всех объявленных переменных. В результате объявление каждой лишней переменной отнимает время при каждом вызове функции.
Ладно, но всё же сколько времени тратится на каждую переменную? Так случилось, что в тот день мой компьютер был настроен для профайлинга, так что я смог измерить затраты времени точно:
На моей машине каждая лишняя переменная замедляет каждый вызов функции на 50 наносекунд. Суммарное замедление растёт линейно с ростом числа лишних переменных, хотя я не проверял случаи с тысячами неиспользуемых переменных, сочтя такое нереалистичым. Кроме того, я не проверял случаи с очень длинными именами переменных: хотя VBscript и ограничивает имена переменных 256 символами — вполне вероятно, что объявление длинных имён отнимет больше времени, чем объявление коротких.

Моя машина — Pentium III 927 МГц, т.е. задержка составляет около 50 тактов (я не могу сейчас измерить число тактов более точно).

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

Вы не упоминаете, на клиенте или на сервере выполняется этот скрипт. Это крайне важно для анализа производительности!

Задержка на объявление переменной связана с выделением памяти в куче, так что на сервере она может расти нелинейно — в зависимости от загруженности кучи другими потоками. Я измерил только непосредственные затраты процессорного времени на объявление переменной; скорее всего, на 8-процессорном сильно нагруженном сервере и в соседстве с другими потоками, интенсивно использующими кучу — полные затраты времени на объявление переменной будут сильно отличаться от указанных.

А теперь я могу вам рассказать, что весь вышеприведённый анализ быстродействия не стоит и гроша, потому что заслоняет совсем другую проблему. Мы не замечаем слона посередине комнаты. Причин, по которым пользователь может выяснять влияние конкретных языковых конструкций VBScript на быстродействие программы, бывает две:
  1. Этот пользователь интересуется разработкой и устройством языков программирования, и хочет обменяться опытом;
    —либо, намного вероятнее,
  2. Этот пользователь пытается оптимизировать свою программу, чтобы она выполнялась быстрее. Ему крайне важно быстродействие своей программы.

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

Никчёмный анализ №2


Если вы хотите, чтобы скрипт работал быстрее, то начинать вам следует уж точно не с удаления 50-наносекундных задержек. Главное в оптимизации — найти самую времяёмкую операцию, и оптимизировать именно её. Например, один вызов функции, использующей необъявленную переменную, будет в сотни раз затратнее, чем объявление неиспользуемой переменной. Один вызов внешнего объекта будет в тысячи раз затратнее. Точно так же вы можете подстригать газон маникюрными ножничками: вы потратите уйму своего времени, но не достигнете никакого видимого результата. Именно в этом разница между «активностью» и «продуктивностью». Работайте продуктивно!

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

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

Но стойте-ка, в этой комнате есть ещё один слон, так что моё второе исследование столь же бессмысленное, как и первое. Для осмысленного анализа быстродействия нам не хватает одного параметра — самого важного:

Насколько плохим код должен быть?


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

Вы, как и я, наверняка видели немало советов навроде «для проверки чётности числа лучше использовать And 1, чем Mod 2, потому что процессор быстрее выполняет команду And, чем деление» — как будто бы операции VBScript компилировались в машинные команды. Люди, выбирающие используемый оператор на основании подобной чепухи, напишут неподдерживаемый, некорректный код. Их программы не будут работать правильно, а хуже неправильной программы не может быть уже ничего — независимо от быстродействия.

Если вы хотите, чтобы ваш код работал быстро — не важно, на скриптовом языке или на любом другом — то пропускайте мимо ушей все такие советы о быстродействии операторов, как и все «анализы» задержки на объявление переменной. Чтобы код работал быстро, не нужно «маленьких хитростей» — нужно проанализировать задачи пользователя, установить требования к программе, затем тщательно измерять производительность программы и вносить планомерные изменения, пока требования не будут достигнуты.
  1. Сосредоточьтесь на задачах пользователя и установите ясные требования к производительности.
  2. Сформулируйте эти требования формально. Что именно критично? Количество выполненных за секунду задач? Задержка перед началом вывода? Время до завершения вывода? Масштабируемость?
  3. Измеряйте производительность всей системы, а не отдельных частей.
  4. Измеряйте производительность после любых изменений.

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

Но для этого необходимо знать требования к быстродействию. Выясните, что именно важно вашим пользователям. Клиентские приложения должны быть отзывчивыми — обработка данных внутри приложения может занимать пять минут, может занимать час, но нажатия кнопок должны обрабатываться в пределах 200 мс, иначе приложение будет казаться зависшим. Веб-приложения должны быть намного быстрее — разница между 25 мс/запрос и 50 мс/запрос составляет 20 запросов в секунду. Но заметит ли пользователь разницу, если 10-килобайтная страница откроется на 25 мс быстрее? Пользователь с модемом на 14 Кбит/сек уж точно не заметит.

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

Вполне может оказаться, что для ваших требований подходит скриптовый язык; тогда помните, что скрипт — это клей, и практически всё время, затрачиваемое на выполнение типичного скрипта — это не выполнение его кода, а вызовы методов внешних объектов. Если вы вынуждены оптимизировать скрипт, и вы уверены, что он должен оставаться скриптом — ищите крупные задержки. Обработка данных в скрипте — это плохо, а мудрёный код — ещё хуже. Не обращайте внимания на объявления переменных, обращайте внимание на внешние вызовы: каждый сэкономленный вызов стоит десятка тысяч микрооптимизаций.

И напоследок: правильный код лучше, чем быстрый. Пишите код как можно проще. Осмысленный код проще понимать и проще поддерживать. Вернёмся к нашему первому примеру с «лишними Dim»: совершенно неважно, что на каждый лишний Dim тратится 50 нс. Но лишняя переменная — это мусор в коде. Она озадачит и запутает программиста, которому придётся этот код поддерживать. Вот почему лишние переменные стоит удалить.
Only registered users can participate in poll. Log in, please.
Ваше отношение к низкоуровневой оптимизации:
3.69% Я пишу на ЯНУ и у меня на учёте каждый байт и каждый такт 91
9.5% Весь код на ЯВУ я пишу с оглядкой на то, в какой машинный код скомпилируется каждая конструкция 234
22.7% Я проверяю получающийся машинный код лишь изредка и лишь отдельные проблемные участки 559
61.39% Меня вовсе не волнует машинный код, исполняемый файл для меня «чёрный ящик» 1512
2.72% Я не пишу код на ЯП, а пишу ТЗ, блок-схемы, UML-диаграммы и т.п. 67
2463 users voted. 778 users abstained.
Tags:
Hubs:
+121
Comments 246
Comments Comments 246

Articles