Стоит ли микрооптимизация времени?
Я разработчик PHP и всегда думал, что микрооптимизации не стоят времени. Если вам действительно нужна эта дополнительная производительность, вы должны либо написать свое программное обеспечение, чтобы оно было архитектурно более быстрым, либо написать расширение C++ для обработки медленных задач (или, что еще лучше, скомпилировать код с помощью HipHop). Тем не менее, сегодня помощник сказал мне, что есть большая разница в
is_array($array)
а также
$array === (array) $array
и я сказал: "Да, это бессмысленное сравнение на самом деле", но он не согласился бы со мной... и он лучший разработчик в нашей компании и отвечает за веб-сайт, который выполняет около 50 миллионов запросов SQL в день -- например. Итак, я задаюсь вопросом: может ли он ошибаться или микрооптимизация действительно стоит времени и когда?
10 ответов
Микрооптимизация того стоит, когда у вас есть доказательства того, что вы оптимизируете узкое место.
Обычно это того не стоит - напишите наиболее читаемый код из всех возможных и используйте реалистичные тесты производительности для проверки производительности. Если и когда вы обнаружите, что у вас есть узкое место, микрооптимизируйте только этот кусочек кода (измеряя по ходу работы). Иногда небольшое количество микрооптимизации может иметь огромное значение.
Но не микрооптимизируйте весь свой код... в итоге его будет гораздо сложнее поддерживать, и вы вполне вероятно обнаружите, что вы либо упустили реальное узкое место, либо что ваши микрооптимизации наносят ущерб производительности, а не помогает.
Ну, для тривиально маленького массива, $array === (array) $array
значительно быстрее, чем is_array($array)
, На порядок более чем в 7 раз быстрее. Но каждый звонок только на порядок 1.0 x 10 ^ -6
секунд (0.000001 seconds
). Так что, если вы не называете это буквально тысячи раз, это не будет стоить того. И если вы звоните это тысячи раз, я бы посоветовал вам сделать что-то не так...
Разница возникает, когда вы имеете дело с большим массивом. поскольку $array === (array) $array
требует, чтобы новая переменная была скопирована, требует, чтобы массив итеративно перебирался для сравнения, это, вероятно, будет ОЧЕНЬ медленнее для большого массива. Например, в массиве с 100 целочисленными элементами, is_array($array)
находится в пределах погрешности (< 2%
) из is_array()
с небольшим массивом (входит в 0.0909
секунд на 10000 итераций). Но $array = (array) $array
очень медленно Только для 100 элементов это уже в два раза медленнее, чем is_array()
(входит в 0.203
секунд). Для 1000 элементов is_array
остался прежним, но сравнение бросков увеличилось до 2.0699
секунд...
Причина, по которой это быстрее для небольших массивов, заключается в том, что is_array()
имеет издержки быть вызовом функции, где операция приведения представляет собой простую языковую конструкцию... И перебор небольшой переменной (в коде C) обычно будет дешевле, чем затраты на вызов функции. Но для больших переменных разница возрастает...
Это компромисс. Если массив достаточно мал, итерация будет более эффективной. Но с ростом размера массива он будет становиться все медленнее (и, следовательно, вызов функции будет быстрее).
Еще один способ посмотреть на это
Другой способ взглянуть на это - изучить алгоритмическую сложность каждого актера.
Давайте посмотрим на is_array()
первый. Это исходный код в основном показывает, что это O(1)
операция. Это означает, что это операция с постоянным временем. Но нам также нужно взглянуть на вызов функции. В PHP вызовы функций с одним параметром массива O(1)
или же O(n)
в зависимости от того, должно ли инициироваться копирование при записи. Если вы позвоните is_array($array)
когда $array
является ссылкой на переменную, копирование при записи будет инициировано, и произойдет полная копия переменной.
И потому is_array()
это лучший случай O(1)
и в худшем случае O(n)
, Но пока вы не используете ссылки, это всегда O(1)
...
С другой стороны, приведенная версия выполняет две операции. Он выполняет приведение, затем проверяет равенство. Итак, давайте посмотрим на каждого в отдельности. Обработчик оператора приведения сначала принудительно копирует входную переменную. Неважно, ссылка это или нет. Так просто используя (array)
оператор кастинга заставляет O(n)
итерация по массиву для его приведения (через вызов copy_ctor).
Затем он преобразует новую копию в массив. Это O(1)
для массивов и примитивов, но O(n)
для объектов.
Затем выполняется идентичный оператор. Обработчик является просто прокси для is_identical_function()
, Теперь is_identical будет короткого замыкания, если $array
это не массив. Таким образом, он имеет лучший случай O(1)
, Но если $array
является массивом, он может снова закорачиваться, если хеш-таблицы идентичны (это означает, что обе переменные являются копиями друг друга при записи). Так что это дело O(1)
также. Но помните, что мы принудительно сделали копию выше, поэтому мы не можем сделать это, если это массив. Так что это O(n)
благодаря zend_hash_compare...
Таким образом, конечный результат - это таблица времени выполнения наихудшего случая:
+----------+-------+-----------+-----------+---------------+
| | array | array+ref | non-array | non-array+ref |
+----------+-------+-----------+-----------+---------------+
| is_array | O(1) | O(n) | O(1) | O(n) |
+----------+-------+-----------+-----------+---------------+
| (array) | O(n) | O(n) | O(n) | O(n) |
+----------+-------+-----------+-----------+---------------+
Обратите внимание, что, похоже, они одинаково масштабируются для ссылок. Они не Они оба масштабируются линейно для указанных переменных. Но постоянный фактор меняется. Например, в ссылочном массиве размером 5 is_array выполнит 5 выделений памяти и 5 копий памяти, а затем 1 проверка типа. Версия Cast, с другой стороны, будет выполнять 5 выделений памяти, 5 копий памяти, затем 2 проверки типов, затем 5 проверок типов и 5 проверок равенства (memcmp()
или т.п). Так n=5
дает 11 операций для is_array
еще 22 опа для ===(array)
...
Сейчас, is_array()
действительно имеет издержки O(1) от выталкивания стека (из-за вызова функции), но это будет доминировать во время выполнения только для очень малых значений n
(мы видели в тесте выше всего 10 элементов массива, чтобы полностью устранить все различия).
Суть
Я бы предложил пойти на удобочитаемость, хотя. я нахожу is_array($array)
быть гораздо более читабельным, чем $array === (array) $array
, Таким образом, вы получаете лучшее из обоих миров.
Скрипт, который я использовал для теста:
$elements = 1000;
$iterations = 10000;
$array = array();
for ($i = 0; $i < $elements; $i++) $array[] = $i;
$s = microtime(true);
for ($i = 0; $i < $iterations; $i++) is_array($array);
$e = microtime(true);
echo "is_array completed in " . ($e - $s) ." Seconds\n";
$s = microtime(true);
for ($i = 0; $i < $iterations; $i++) $array === (array) $array;
$e = microtime(true);
echo "Cast completed in " . ($e - $s) ." Seconds\n";
Изменить: для записи, эти результаты были с 5.3.2 на Linux...
Edit2: исправлена причина, по которой массив работает медленнее (это связано с повторным сравнением, а не с памятью). См. Функцию сравнения для кода итерации...
Стоит ли микрооптимизация времени?
Нет, если это не так.
Другими словами, априори ответ "нет", но после того, как вы знаете, что конкретная строка кода потребляет здоровый процент времени, тогда и только тогда стоит оптимизировать.
Другими словами, сначала профиль, потому что иначе у вас нет этих знаний. Это метод, на который я полагаюсь, независимо от языка или ОС.
Добавлено: Когда многие программисты обсуждают производительность, от экспертов до, они склонны говорить о том, "где" программа тратит свое время. В том "где" скрывается двусмысленная двусмысленность, которая уводит их от вещей, которые могли бы сэкономить больше всего времени, а именно к сайтам вызова функций. В конце концов, "вызов Main" в верхней части приложения - это "место", в котором программа почти никогда не "находится", но оно отвечает за 100% времени. Теперь вы не собираетесь избавляться от "Call Main", но есть почти всегда другие вызовы, от которых вы можете избавиться. Когда программа открывает или закрывает файл, или форматирует некоторые данные в текстовую строку, или ожидает соединения с сокетом, или "новой"- с помощью фрагмента памяти, или передает уведомление по большой структуре данных, тратить много времени на вызовы функций, но это ли "где"? В любом случае, эти вызовы быстро обнаруживаются с помощью образцов стека.
Как следует из клише, микрооптимизация, как правило, стоит времени только в самых маленьких, наиболее критичных для производительности горячих точках вашего кода, только после того, как вы докажете, что это узкое место. Тем не менее, я хотел бы немного прояснить это, указать на некоторые исключения и области недопонимания.
Это не означает, что производительность вообще не должна рассматриваться заранее. Я определяю микрооптимизацию как оптимизацию, основанную на низкоуровневых деталях компилятора / интерпретатора, аппаратного обеспечения и т. Д. По определению микрооптимизация не влияет на сложность big-O. Макрооптимизации следует рассматривать заранее, особенно когда они оказывают серьезное влияние на проектирование высокого уровня. Например, можно с уверенностью сказать, что если у вас есть большая, часто используемая структура данных, O(N) линейный поиск не собирается ее сокращать. Даже вещи, которые являются только постоянными терминами, но имеют большие и очевидные накладные расходы, возможно, стоит рассмотреть заранее. Два больших примера - это чрезмерное распределение памяти / копирование данных и вычисление одного и того же дважды, когда вы могли бы вычислить его один раз и сохранить / повторно использовать результат.
Если вы делаете что-то, что было сделано ранее в несколько ином контексте, могут быть некоторые узкие места, которые настолько хорошо известны, что разумно рассмотреть их заранее. Например, недавно я работал над реализацией алгоритма FFT (быстрое преобразование Фурье) для стандартной библиотеки D. Так как раньше было написано так много БПФ на других языках, очень хорошо известно, что самым большим узким местом является производительность кеша, поэтому я сразу же приступил к проекту, думая о том, как это оптимизировать.
В общем, вы не должны писать какие-либо оптимизации, которые делают ваш код более уродливым или трудным для понимания; в моей книге это определенно относится к этой категории.
Гораздо сложнее вернуться назад и изменить старый код, чем писать новый, потому что вы должны провести регрессионное тестирование. Таким образом, в целом, ни один код, уже находящийся в производстве, не должен быть изменен по необоснованным причинам.
PHP является настолько невероятно неэффективным языком, что если у вас есть проблемы с производительностью, вам, вероятно, стоит подумать о рефакторинге горячих точек, чтобы они все равно выполняли меньше кода PHP.
Поэтому я бы сказал, что в целом нет, а в данном случае нет, а в случаях, когда вам это абсолютно необходимо, И измеряли, что это имеет доказуемое значение, И это самый быстрый выигрыш (низко висящий фрукт), да.
Разумеется, разбрасывать подобные микрооптимизации по всему существующему, работающему, протестированному коду - ужасная вещь, она определенно привнесет регрессии и почти наверняка не даст заметной разницы.
У нас было одно место, где оптимизация была действительно полезной.
Вот некоторое сравнение:
is_array($v)
: 10 сек
$v === (array)$v
: 3,3 сек
($v.'') === 'Array'
: 2,6 сек
Последний выполняет приведение к строке, массив всегда приводится к строке со значением "массив". Эта проверка будет неправильной, если $v является строкой со значением 'Array' (в нашем случае это никогда не происходит).
Ну, я собираюсь предположить, что is_array($array)
является предпочтительным способом, и $array === (array) $array
якобы более быстрый способ (который поднимает вопрос, почему не is_array
реализовано с использованием этого сравнения, но я отвлекся).
Я вряд ли когда-нибудь вернусь к своему коду и вставлю микрооптимизацию*, но я буду часто вставлять их в код при написании, при условии:
- это не замедляет мою печать.
- цель кода все еще ясна.
Эта конкретная оптимизация терпит неудачу в обоих случаях.
* Ладно, на самом деле я знаю, но это больше связано с тем, что я имею пристрастие к ОКР, а не хорошие практики разработки.
Микрооптимизация не стоит. Читаемость кода гораздо важнее, чем микрооптимизация.
Отличная статья о бесполезной микрооптимизации от Фабьена Потенцера (создателя фреймворка Symfony):
печать против эха, который быстрее?
Print использует еще один код операции, потому что он действительно что-то возвращает. Мы можем сделать вывод, что эхо быстрее, чем печать. Но один код операции ничего не стоит, на самом деле ничего. Даже если в сценарии есть сотни вызовов для печати. Я попробовал на свежую установку WordPress. Сценарий останавливается до того, как он заканчивается "Ошибка шины" на моем ноутбуке, но количество кодов операций уже превысило 2,3 миллиона. Достаточно сказано.
Ну, есть больше вещей, чем скорость, чтобы принять во внимание. Когда вы читаете эту "более быструю" альтернативу, вы сразу думаете: "О, это проверка, чтобы увидеть, является ли переменная массивом", или вы думаете, "...wtf"?
Потому что действительно - при рассмотрении этого метода, как часто он называется? Какое точное преимущество в скорости? Это складывается, когда массив больше или меньше? Нельзя делать оптимизацию без тестов.
Кроме того, не следует делать оптимизации, если они снижают читабельность кода. Фактически, уменьшение этого количества запросов на несколько сотен тысяч (а это часто проще, чем можно было бы подумать) или их оптимизация, если это применимо, было бы намного, намного более выгодным для производительности, чем эта микрооптимизация.
Кроме того, не пугайтесь опыта парня, как говорили другие, и думайте сами.
ИМХО микрооптимизации на самом деле даже более актуальны, чем алгоритмические оптимизации сегодня, если вы работаете в области, критически важной для производительности. Это может быть очень важно, если бы многие люди на самом деле не работали в критически важных для производительности областях даже для программного обеспечения, критичного к производительности, поскольку они могли просто делать высокоуровневые вызовы в стороннюю библиотеку, которая выполняет реальную работу, критичную для производительности. Например, в наши дни многие люди, пытающиеся написать программное обеспечение для изображений или видео, могут написать не критичный к производительности код, выражающий их желание на уровне изображения, без необходимости вручную вручную проходить несколько миллионов пикселей со скоростью более 100 кадров в секунду. Библиотека делает это для них.
Когда я говорю, что микрооптимизации сегодня более актуальны, чем алгоритмические, я не имею в виду, что, скажем, распараллеленный SIMD-код, который минимизирует пропадание кэша при использовании пузырьковой сортировки, превзойдет интросорт или радикальную сортировку. Я имею в виду, что профессионалы не сортируют пузырьки больших размеров.
Если вы сегодня используете какой-либо достаточно высокоуровневый язык, в том числе C++, у вас уже есть достаточно эффективные универсальные структуры данных и алгоритмы. Нет никакого оправдания, если только вы не начинающий студент CS, просто намочив свои ноги и заново изобретая самые примитивные колеса для применения квадратичной сортировки сложности к массивным входным размерам или поискам с линейным временем, которые могут быть выполнены в постоянном времени с соответствующими данными структур.
Поэтому, как только вы пройдете этот начальный уровень, критически важные для производительности приложения по-прежнему будут иметь сильно отличающиеся характеристики производительности. Зачем? Почему одно программное обеспечение для обработки видео имеет в три раза большую частоту кадров и больше интерактивных предварительных просмотров видео, чем другое, если разработчики не делают ничего чрезвычайно глупого алгоритмически? Почему один сервер, который делает очень похожую вещь, мог бы обрабатывать десять раз запросы с одним и тем же оборудованием? Почему это программное обеспечение загружает сцену за 5 секунд, в то время как другая загружает те же данные за 5 минут? Почему эта красивая игра имеет шелковистую плавную и стабильную частоту кадров, в то время как другая уродливая, более примитивная, с графикой и освещением, и заикается здесь и там, занимая вдвое больше памяти?
И это сводится к микрооптимизации, а не к алгоритмическим различиям. Кроме того, наша иерархия памяти сегодня настолько искажена в производительности, что делает предыдущие алгоритмы, которые считались хорошими пару десятилетий назад, уже не такими хорошими, если они демонстрируют плохую локальность ссылок.
Поэтому, если вы хотите писать конкурентоспособное и эффективное программное обеспечение сегодня, гораздо чаще, чем когда-либо, оно будет сводиться к таким вещам, как многопоточность, SIMD, GPU, GPGPU, улучшенная локальность ссылок с лучшими шаблонами доступа к памяти (циклическое разбиение на блоки, SoA, hot/ холодное разделение полей и т. д.), может быть, даже оптимизация для прогнозирования ветвлений в экстремальных случаях и т. д., не так много алгоритмических прорывов, если только вы не занимаетесь чрезвычайно неизведанной территорией, где до этого не сталкивались программисты.
Иногда до сих пор существуют алгоритмические открытия, которые могут изменить игру, например, отслеживание воксельных конусов. Но это исключения, и люди, которые их придумывают, часто вкладывают свою жизнь в исследования и разработки (как правило, это не люди, которые пишут и поддерживают крупномасштабные кодовые базы), и все же сводится к микрооптимизации, можно ли применять трассировку воксельных конусов. в режиме реального времени, такие как игры или нет. Если вы плохо разбираетесь в микрооптимизации, вы просто не получите адекватную частоту кадров даже с помощью этих алгоритмических прорывов.