Реймонд Эрик Стивен
Шрифт:
Этот весьма выдержанный в духе Дзэн совет верен по нескольким причинам. Одной из них является экспоненциальный эффект закона Мура — самый разумный, дешевый и часто самый быстрый способ достичь прироста производительности заключается в том, чтобы подождать несколько месяцев, пока целевое аппаратное обеспечение станет более мощным. Учитывая ценовое соотношение между аппаратным обеспечением и временем программиста, почти всегда существуют лучшие варианты использования времени, чем оптимизация уже работающей системы.
Данная точка зрения может быть обоснована математически. Почти никогда не следует выполнять оптимизацию, которая сокращает использование ресурсов просто на постоянную величину. Гораздо разумнее сконцентрировать усилия на случаях, в которых можно сократить среднее время запуска или использование пространства с O(n²) до O(n) или О(n log n) [108] или подобным образом понизить порядок. Согласно закону Мура, линейный прирост производительности достаточно быстро уменьшается [109] .
108
Для читателей, не знакомых с О-нотацией: она представляет собой способ указания зависимости между средним временем выполнения алгоритма и размерами его входных данных. Алгоритм O(1) выполняется за постоянное время. Алгоритм О(n) выполняется за время, которое можно вычислить по формуле: An + С, где А — некоторый неизвестный постоянный коэффициент пропорциональности, а С — неизвестная константа, представляющая время установки. Линейный поиск определенного значения в списке представляет собой алгоритм типа О(n). Алгоритм О(n²) выполняется за время An² плюс величина более низкого порядка (которая может быть линейной либо логарифмической или любой другой функцией ниже квадратичной). Поиск повторяющихся значений в списке (примитивным методом, без сортировки списка) является алгоритмом О(n²). Аналогично, алгоритмы порядка О(n³) имеют среднее время выполнения, вычисляемое по кубической формуле. Такие алгоритмы часто слишком медленны для практического применения. Порядок O(log n) типичен для поиска по дереву. Взвешенный выбор алгоритма часто может сократить время выполнения с O(п²) до О(log n). Иногда, когда требуется рассчитать использование алгоритмом памяти, можно заметить, что оно изменяется как O(1) или О(n), или O(n²). Как правило, алгоритмы с использованием памяти О(n²) или более высокого порядка являются непрактичными.
109
Удвоение мощности в течение каждых 18 месяцев, обычно цитируемое в контексте закона Мура, подразумевает, что можно достичь 26% прироста производительности просто путем приобретения нового аппаратного обеспечения через 6 месяцев.
Существует другая весьма конструктивная форма отказа от оптимизации — не писать код. Скорость работы программы не может быть уменьшена кодом, которого в ней нет, она может быть уменьшена кодом, который в ней есть, если он менее эффективен, чем мог бы быть. Однако это уже другая проблема.
12.2. Измерения перед оптимизацией
Когда имеются реальные доказательства того, что разрабатываемое приложение работает слишком медленно, тогда (и только тогда) наступает время обдумать оптимизацию кода. Однако перед этим необходимо сделать нечто большее — провести измерения.
Читателям следует вспомнить шесть правил Роба Пайка, описанных в главе 1. Один из первых уроков, который усвоили программисты Unix, заключается в том, что интуиция — неверный советчик при определении местоположения "бутылочных горлышек", причем даже для того, кто очень хорошо знает свой код. Unix-системы, в отличие от большинства других операционных систем, обычно поставляются вместе с профайлерами (profiler — подпрограмма протоколирования), и их стоит использовать.
Чтение результатов работы профайлера вполне можно сравнить с искусством. Существует несколько периодически повторяющихся проблем: первая — погрешность измерения, вторая — влияние налагаемых внешних задержек, третья — перевес верхних узлов графа вызовов.
Фундаментальной является проблема погрешности измерений. Профайлеры выполняют свои функции, внедряя инструкции, которые фиксируют время выполнения в точках входа и выхода из подпрограмм, а также в фиксированных интервалах во внутреннем коде программ. Выполнение данных инструкций само по себе отнимает некоторое время. В результате сокращается разброс времени вызовов: очень короткие подпрограммы часто выглядят более дорогостоящими, чем они есть на самом деле, с большим количеством шума в их относительном времени вызова, тогда как для более длинных подпрограмм издержки измерения незаметны.
Принимая во внимание погрешность измерения, разумно предположить, что значения времени, указанные для самых быстрых, кратчайших подпрограмм, из-за шума будут завышены. Их выполнение также потребует большого количества времени, если они будут вызываться очень часто, однако, следует уделить должное внимание статистическим данным по количеству их вызовов.
Проблема внешней задержки также является фундаментальной. Существуют различные виды задержек и искажений, которые могут возникать вне поля зрения профайлера. Простейшим примером являются издержки операций с непредсказуемой задержкой: доступ к дискам и сетям, заполнение кэша, переключение контекста процессов и им подобные. Проблема заключается не столько в возникновении данных задержек — возможно, именно их и требуется измерить, особенно если основное внимание уделяется производительности системы в целом, а не просто регулировке критически важного внутреннего цикла. Реальная проблема состоит в том, что они содержат случайный компонент, а это означает, что результаты любого отдельного запуска профайлера могут быть не самыми достоверными.
Одним из способов минимизации влияния указанных источников шума и получения более четкой картины утечки времени в среднем случае является сложение результатов от множества запусков профайлера. Существует множество весомых причин для создания средств тестирования и тестовых нагрузок до оптимизации разрабатываемых программ. Наиболее важной причиной, как правило, гораздо более важной, чем регулировка производительности, является то, что впоследствии, по мере модификации программы, можно использовать возвратное тестирование ее корректности. После того как это сделано, возможность выполнять профилирование повторяющихся тестов под нагрузкой является хорошим побочным эффектом, который часто предоставляет более точную информацию, чем несколько запусков вручную.
Различные факторы склонны перекладывать затраты времени на вызывающие программы, а не на вызываемые, что увеличивает вес верхних узлов графа вызовов. Например, издержки вызова функции часто относят к вызывающей программе (так это или нет, в некоторой степени зависит от архитектуры конкретной машины и от того, где профайлеру разрешено размещать пробы). Макросы и встраиваемые функции, в случае если компилятор их поддерживает, не показываются в отчете профайлера вообще. Расходуемое ими время относится к вызывающей функции.