Файл: Руководство по стилю программирования и конструированию по.pdf
ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 30.11.2023
Просмотров: 875
Скачиваний: 2
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
1 ... 71 72 73 74 75 76 77 78 ... 104
ГЛАВА 26 Методики оптимизации кода
621
и требует преобразований типов. Во втором случае преобразования типов не нужны:
x = 3.14
i = 5
Результаты в очередной раз указывают на большие различия между компиляторами:
Время выполнения
Время выполнения
оптимизированного Экономия Соотношение
Язык
кода до оптимизации кода
времени
быстродействия
C++
1,11 0,000 100%
Не поддается измерению
C#
1,49 1,48
<1%
1:1
Java
1,66 1,11 33%
1,5:1
Visual Basic 0,721 0,000 100%
Не поддается измерению
PHP
0,872 0,847 3%
1:1
Предварительное вычисление результатов
При низкоуровневом проектировании часто приходится решать, вычислять ли результаты по ходу дела или лучше вычислить их один раз, сохранить и извле- кать по мере надобности. Если результаты используются много раз, второй вари- ант часто предпочтительнее.
Этот выбор проявляется несколькими способами. На самом простом уровне вы можете вычислить часть выражения вне цикла вместо того, чтобы вычислять его внутри. Пример этого я приводил выше. На более сложном уровне вы можете вычислить табличные данные один раз при запуске программы и использовать их позднее; вы также можете сохранить результаты в файле данных или встроить их в программу.
Например, работая над игрой «звездные войны», програм- мисты сначала вычисляли коэффициенты гравитации для разных расстояний от Солнца. Эти вычисления были ресур- соемкими и снижали производительность программы. Од- нако число разных расстояний, используемых в игре, было небольшим, поэтому разработчики в итоге вычислили коэффициенты гравитации предварительно и сохранили их в массиве из 10 элементов. Получение коэффициентов из массива оказалось гораздо более быстрым, чем их вычисление.
Допустим, у вас есть метод, вычисляющий сумму, которую нужно уплатить при погашении ссуды на покупку автомобиля. Код подобного метода мог бы выгля- деть так:
Пример сложного выражения, которое можно вычислить предварительно (Java)
double ComputePayment(
long loanAmount,
int months,
Перекрестная ссылка Об исполь- зовании табличных данных вме- сто сложной логики см. главу 18.
622
ЧАСТЬ V Усовершенствование кода double interestRate
) {
return loanAmount /
(
( 1.0 - Math.pow( ( 1.0 + ( interestRate / 12.0 ) ), -months ) ) /
( interestRate / 12.0 )
);
}
Формула вычисления платежей по ссуде сложна и довольно дорога. Помещение информации в таблицу, наверное, сделало бы вычисление более дешевым.
Насколько крупной была бы эта таблица? Переменной с наибольшим диапазоном является переменная
loanAmount. Переменная interestRate может принимать зна- чения от 5% до 20% с шагом 0,25%, что дает нам 61 значение. Переменная
months
может иметь значение от 12 до 72, или 61 значение. Значение переменной
loan-
Amount, вероятно, может находиться в пределах от 1000 до 100 000 долларов, и вам едва ли удастся сохранить все эти значения в таблице.
Однако большая часть выражения не зависит от
loanAmount, поэтому параметры действительно отвратительной части (знаменатель более крупного выражения)
можно сохранить в таблице, индексируемой значениями
interestRate и months.
Значение
loanAmount нужно будет вычислять:
Пример предварительного вычисления сложного выражения (Java)
double ComputePayment(
long loanAmount,
int months,
double interestRate
) {
Новая переменная interestIndex используется как индекс массива loanDivisor.
int interestIndex =
Math.round( ( interestRate - LOWEST_RATE ) * GRANULARITY * 100.00 );
return loanAmount / loanDivisor[ interestIndex ][ months ];
}
Итак, сложное вычисление мы заменили вычислением индекса массива и одним обращением к массиву. К чему же это привело?
Время выполнения
Время выполнения
оптимизированного Экономия Соотношение
Язык
кода до оптимизации кода
времени
быстродействия
Java
2,97 0,251 92%
10:1
Python
3,86 4,63
–20%
1:1
В зависимости от обстоятельств вы должны были бы предварительно вычислять массив
loanDivisor при инициализации программы или читать его из файла. Вы также могли бы инициализировать массив нулями, вычислять каждый элемент при его первом запросе, сохранять его, а впоследствии просто извлекать из массива.
Это было бы одной из форм кэширования, которое обсуждалось выше.
>
ГЛАВА 26 Методики оптимизации кода
623
Предварительное вычисление выражения не обязывает вас создавать таблицу —
возможен и иной вариант. Допустим, у вас есть код, вычисляющий взносы, кото- рые нужно уплатить при погашении ссуд на разную сумму:
Пример второго сложного выражения,
которое можно вычислить предварительно (Java)
double ComputePayments(
int months,
double interestRate
) {
for ( long loanAmount = MIN_LOAN_AMOUNT; loanAmount < MAX_LOAN_AMOUNT;
loanAmount++ ) {
payment = loanAmount / (
( 1.0 – Math.pow( 1.0+(interestRate/12.0), - months ) ) /
( interestRate/12.0 )
);
Следующий код делал бы что-то с переменной payment; что именно — в данном примере не важно.
}
}
Даже без таблицы, вы можете предварительно вычислить сложную часть выраже- ния вне цикла и использовать найденное значение внутри цикла:
Пример предварительного вычисления второго сложного выражения (Java)
double ComputePayments(
int months,
double interestRate
) {
long loanAmount;
Предварительное вычисление части выражения.
double divisor = ( 1.0 – Math.pow( 1.0+(interestRate/12.0). - months ) ) /
( interestRate/12.0 );
for ( long loanAmount = MIN_LOAN_AMOUNT; loanAmount <= MAX_LOAN_AMOUNT;
loanAmount++ ) {
payment = loanAmount / divisor;
}
}
Этот вид оптимизации похож на уже рассмотренные нами методики, предпола- гающие вынесение обращений к массиву и разыменований указателей за преде- лы цикла. Результаты оптимизации кода Java в данном случае сравнимы с резуль- татами предыдущего вида оптимизации, основанного на предварительном вычис- лении табличных данных:
>
>
624
ЧАСТЬ V Усовершенствование кода
Время выполнения
Время выполнения
оптимизированного Экономия Соотношение
Язык
кода до оптимизации кода
времени
быстродействия
Java
7,43 0,24 97%
30:1
Python
5,00 1,69 66%
3:1
В отличие от первой попытки оптимизации быстродействие кода Python сейчас также выросло. Это происходит довольно часто: если один вид оптимизации не приводит к желаемым результатам, другой, казалось бы, аналогичный вид оказы- вается очень эффективным.
Оптимизация программы путем предварительного вычисления выражений име- ет несколько вариантов:
쐽
вычисление результатов до выполнения программы и связывание их с констан- тами во время компиляции;
쐽
вычисление результатов до выполнения программы и присвоение их перемен- ным, используемым в период выполнения;
쐽
вычисление результатов до выполнения программы и сохранение их в файле,
загружаемом в период выполнения;
쐽
однократное вычисление результатов при запуске программы и их использо- вание во всех последующих случаях;
쐽
вычисление как можно большего числа значений до начала цикла, позволяю- щее свести к минимуму объем работы, выполняемой внутри цикла;
쐽
вычисление результатов при возникновении первой потребности в них и их сохранение, позволяющее получить результаты, когда они понадобятся снова.
Устранение часто используемых подвыражений
Если какое-то выражение повторяется в коде несколько раз, присвойте его зна- чение переменной и используйте переменную вместо вычисления выражения в нескольких местах. Подвыражение, которое можно устранить, уже встречалось нам в предыдущем подразделе:
Пример часто используемого подвыражения (Java)
payment = loanAmount / (
( 1.0 – Math.pow( 1.0 + ( interestRate / 12.0 ), - months ) ) /
( interestRate / 12.0 )
);
Вместо двукратного вычисления выражения
interestRate/12.0 вы можете присво- ить его переменной и обратиться к ней два раза. Если вы присвоите переменной удачное имя, этот вид оптимизации не только повысит производительность кода,
но и улучшит его удобочитаемость. Вот оптимизированный код:
Пример устранения часто используемого подвыражения (Java)
monthlyInterest = interestRate / 12.0;
payment = loanAmount / (
ГЛАВА 26 Методики оптимизации кода
625
( 1.0 – Math.pow( 1.0 + monthlyInterest, - months ) ) /
monthlyInterest
);
Результаты не впечатляют:
Время выполнения
Время выполнения
Экономия
Язык
кода до оптимизации
оптимизированного кода
времени
Java
2,94 2,83 4%
Python
3,91 3,94
–1%
По-видимому, метод
Math.pow() настолько дорог, что он перекрывает выгоду уст- ранения подвыражения. Возможно также, что подвыражение уже было устранено компилятором. Если бы подвыражение составляло не такую малую часть стоимо- сти всего выражения или если бы оптимизатор компилятора был менее эффек- тивен, этот вид оптимизации, наверное, оказал бы большее влияние на произво- дительность.
26.5. Методы
Одним из самых эффективных способов оптимизации кода является грамотная декомпозиция программы на методы.
Небольшие, хорошо определенные методы делают програм- му компактнее, устраняя повторяющиеся фрагменты кода. Они упрощают опти- мизацию, потому что рефакторинг одного метода улучшает все методы, которые его вызывают. Небольшие методы относительно легко переписать на низкоуров- невом языке. Объемные хитроумные методы понять сложно, а после переписы- вания их на низкоуровневом языке вроде ассемблера это вообще невыполнимо.
Встраивание методов
На заре программирования вызывать методы на некоторых компьютерах было крайне дорого. Вызов метода означал, что ОС должна выгрузить программу, за- грузить каталог методов, загрузить конкретный метод, выполнить метод, выгру- зить метод и снова загрузить вызвавший метод. Все это потребляло много ресур- сов и замедляло программу.
Современные компьютеры облагают вызовы методов гораздо меньшей пошлиной.
Например, встраивание метода копирования строк приводит к таким результатам:
Время выполнения
Время выполнения
Экономия
Язык
метода
встроенного кода
времени
C++
0,471 0,431 8%
Java
13,1 14,4
–10%
В некоторых случаях вы можете сэкономить несколько наносекунд, встроив код метода в программу, используя ключевое слово
inline языка C++ или аналогичную возможность. Если ваш язык не поддерживает
inline непосредственно, но имеет препроцессор макросов, вы можете встраивать код при помощи макроса, вклю- чая и выключая встраивание по требованию. Однако современные компьютеры
Перекрестная ссылка Об исполь- зовании методов см. главу 7.
626
ЧАСТЬ V Усовершенствование кода
(под «современными» я понимаю любые машины, с которыми вы можете столк- нуться в своей работе), при вызове метода почти не тратят дополнительных ре- сурсов. Как показывает пример, встроив код, вы можете как улучшить производи- тельность, так и ухудшить ее.
26.6. Переписывание кода
на низкоуровневом языке
Давняя мудрость, которую не стоит оставлять без внимания, гласит, что при низ- ком быстродействии код следует переписать на языке низкого уровня. Если вы пишете на C++, языком низкого уровня может быть ассемблер, если на Python —
C. Переписывание кода на низкоуровневом языке обычно положительно влияет и на быстродействие кода, и на его объем. Типичный подход к оптимизации при помощи низкоуровневого языка таков.
1. Напишите все приложение на высокоуровневом языке.
2. Выполните полное тестирование приложения и проверьте его корректность.
3. Если производительность недостаточна, выполните про- филирование приложения с целью выявления горячих то- чек. Так как около 50% времени выполнения программы обычно приходится примерно на 5% кода, горячими точ- ками обычно будут небольшие фрагменты программы.
4. Перепишите несколько небольших фрагментов на низ- коуровневом языке для повышения общей производительности программы.
Последуете ли вы по этому проторенному пути, зависит от того, насколько хоро- шо вы программируете на низкоуровневых языках, насколько хорошо проблема подходит для решения на низкоуровневом языке, а также от степени вашего от- чаяния. Сам я впервые применил эту методику при реализации алгоритма Data
Encryption Standard, о чем я упоминал в главе 25. Я перепробовал все виды опти- мизации, о которых когда-либо слышал, но программа все равно работала вдвое медленнее, чем нужно. Мне ничего не оставалось делать, кроме как переписать часть программы на ассемблере. Не имея особого опыта программирования на ассемблере, я по сути ограничился простым переводом кода с высокоуровневого языка на ассемблер, но даже этот примитивный подход ускорил выполнение про- граммы на 50%.
Рассмотрим переписывание на ассемблере метода, преобразующего двоичные данные в символы ASCII верхнего регистра. В следующем примере показан соот- ветствующий код Delphi:
Пример кода на Delphi, который лучше переписать на ассемблере
procedure HexExpand(
var source: ByteArray;
var target: WordArray;
byteCount: word
);
Перекрестная ссылка Подроб- нее о том, что основная часть времени выполнения програм- мы приходится на малый про- цент кода, см. подраздел «Прин- цип Парето» раздела 25.2.