Uploaded by Dmitry Molkov

4080

advertisement
Саратовский национальный исследовательский государственный университет
имени Н.Г. Чернышевского
Факультет компьютерных наук и информационных технологий
Кафедра информатики и программирования
ТЕОРЕТИЧЕСКИЕ ОСНОВЫ ОПТИМИЗАЦИИ ПРОГРАММНОГО КОДА
работа выполнена в рамках курсового проекта по дисциплине
«Структуры и алгоритмы компьютерной обработки данных»
Выполнила: Батталова Нур Алия Илдаровна
Научный руководитель: Кудрина Е.В., доцент кафедры информатики и программирования
Саратов, 2017
СОДЕРЖАНИЕ
ВВЕДЕНИЕ........................................................................................................................................3
1
ТЕРМИН «ОПТИМИЗАЦИЯ КОДА» И СВЯЗАННЫЕ С НИМ ПОНЯТИЯ ..............5
2
ВИДЫ ОПТИМИЗАЦИИ ПРОГРАММНОГО КОДА ......................................................6
3
ПОДХОД К ОПТИМИЗАЦИИ ПРОГРАММНОГО КОДА .............................................8
4
МЕТОДИКИ ОПТИМИЗАЦИИ КОДА................................................................................9
4.1
Логические выражения ....................................................................................................9
4.2
Использование выражение ............................................................................................11
4.3
Циклы ................................................................................................................................13
4.4
Методы...............................................................................................................................16
4.5
Изменения типов данных ...............................................................................................17
ЗАКЛЮЧЕНИЕ ..............................................................................................................................19
СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ.................................................................20
2
ВВЕДЕНИЕ
В современном мире разработка программного обеспечения (ПО) превратилась в
одну из самых дорогостоящих индустрий, и любые ошибки и недочеты в процессе его
создания могут привести к нежелательным результатам. Написание запутанного кода
чревато проблематичным изменением и сопровождением готового продукта. Ошибки, не
выявленные в ходе тестирования ПО, приводят к снижению надежности и затягиванию
сроков его внедрения. Поэтому актуальность разработки совершенного кода очень высока,
так как она позволяет повысить его надежность. Очевидно, что такой код должен быть
максимально оптимальным.
Примитивный, но правильный код, написанный программистом, во многих случаях
может быть усовершенствован. Чаще всего причиной является то, что выбранный алгоритм,
является шаблонным и не учитывает условия поставленной задачи, то есть транслирует
языковые выражения вне зависимости от их смысла в определенные последовательности
команд. Формальный алгоритм не различает особые случаи и не использует их выгод. Выбор
такого подхода приводит к результатам, которые лишь отчасти отвечают требованиям
экономии памяти и скорости выполнения.
Для того чтобы сгенерировать код, который использует имеющиеся команды и
ресурсы машины с наибольшей эффективностью, должны быть использованы более сложные
схемы трансляции. Они называются оптимизациями, а использующие их компиляторы –
оптимизирующими компиляторами. Так же важно придерживаться правила 10/90, которое
гласит, что 10% времени потраченное на планирование до начала работы, экономит 90%
времени при решении поставленных задач.
Архитектурный дизайн системы особенно сильно влияет на её производительность.
Однако выбор алгоритма влияет на эффективность больше, чем любой другой элемент
дизайна. Более сложные алгоритмы и структуры данных могут хорошо оперировать с
большим количеством элементов, в то время как простые алгоритмы подходят для
небольших объёмов данных – накладные расходы на инициализацию более сложного
алгоритма могут перевесить выгоду от его использования [1, c.5].
Чем больше памяти использует программа, тем быстрее она обычно выполняется.
Например, сортировка ступенчатого массива обычно выполняется построчно – программа
читает каждую строку, сортирует её, а затем выводит эту строку. Такая программа хорошо
экономит
память,
т.к.
использует
её
только
для
хранения
одной
строки,
но
производительность программы обычно очень плохая. Производительность может быть
значительно улучшена чтением целого файла и записью потом отсортированного результата.
3
Однако такой способ использует больше памяти. Кэширование результата также
эффективно, однако требует большего количества памяти для использования.
Цель данной работы – изучить теоретические основы оптимизации программного
кода.
Поставленная цель определила следующие задачи:
1.
Рассмотреть термин «оптимизация кода» и связанные с ним понятия.
2.
Изучить виды и подход к оптимизации кода.
3.
Познакомиться с методиками оптимизации кода.
4
1
ТЕРМИН «ОПТИМИЗАЦИЯ КОДА» И СВЯЗАННЫЕ С НИМ ПОНЯТИЯ
Оптимизация кода – это один из способов преобразования кода, приводящий к
улучшению его характеристик и повышению производительности программы. Среди целей
оптимизации можно выделить уменьшение размера кода, объема используемой оперативной
памяти, повышение скорости выполнения программы, уменьшение количества операций
ввода – вывода. Так как под оптимизацией понимается внесение незначительных поправок,
то есть изменение одного класса, одного метода или всего лишь нескольких строк кода.
Поэтому
какие-либо
крупные
изменения
проекта,
приводящие
к
повышению
производительности оптимизацией не считаются.
Существует требование, которые обычно предъявляется к методу оптимизации –
оптимизированная программа должна иметь тот же результат и побочные эффекты на тех же
входных данных, что и неоптимизированная программа. Тем не менее, если изменения
поведения программы, не имеет большого значения на фоне выигрыша за счет
использования оптимизации, то данное требование может и не играть главной роли.
Кроме того, не существует универсального решения, которое подходило бы ко всем
случаям, поэтому приходится использовать альтернативные решения, для оптимизации
только ключевых параметров. Как правило, необходимые ресурсы для достижения
требуемого результата, то есть получения полностью оптимальной программы, которую
невозможно дальше улучшить, превышают выгоду, которую можно получить, затрачивая эти
ресурсы. Именно поэтому оптимальные программы не создают просто потому, что
некоторый процесс оптимизации может закончиться раньше. Как показывает практика, в
большинстве случаев даже при этом достигаются значительные улучшения [2, c.153].
Встречаются ситуации, когда оптимизированный код вручную, оказывается менее
эффективнее кода, сгенерированного компилятором.
Каждый этап от проектирования до оптимизации кода допускает существенное
повышение производительности программного обеспечения [3, c. 576].
Стоит заметить, что оптимизация кода – это не самый эффективный способ
повышения производительности, более того это не самый легкий способ повысить
производительность: легче купить новое оборудование или компилятор с улучшенным
модулем оптимизации. Так же это не самый дешевый способ: на оптимизацию кода вручную
изначально уходит много времени, а потом оптимизированный код труднее сопровождать.
Однако оптимизация кода привлекательна по ряду причин. Например, ускорить
выполнение метода в 10 раз путем изменения всего лишь нескольких его строк. Кроме того,
5
овладение мастерством написания эффективного кода – признак превращение в серьезного
программиста.
Оптимизация в основном фокусируется на одиночном или повторном времени
выполнения, использовании памяти, дискового пространства, пропускной способности или
некотором другом ресурсе. Это обычно требует компромиссов (tradeoff) – один параметр
оптимизируется за счёт других. Например, увеличение размера программного кэша чеголибо улучшает производительность времени выполнения, но также увеличивает потребление
памяти. Другие распространённые компромиссы включают прозрачность кода и его
выразительность, почти всегда ценой деоптимизации. Сложные специализированные
алгоритмы требуют больше усилий по отладке и увеличивают вероятность ошибок.
Оптимизацию
производительности
следует
отличать
от
рефакторинга.
Цель
рефакторинга – сделать код программы более легким для понимания. Как и оптимизация,
рефакторинг обычно не изменяет поведение программы. Но оптимизация часто затрудняет
понимание кода, что противоположно рефакторингу.
2
ВИДЫ ОПТИМИЗАЦИИ ПРОГРАММНОГО КОДА
Оптимизация кода может проводиться, как и вручную, программистом, так и
автоматизировано. В последнем случае оптимизатор может быть, как отдельным
программным средством, так и быть встроенным в компилятор [4, c.3].
Хороший оптимизирующий компилятор может повысить быстродействие кода на 40 и
более процентов, тогда как многие из методик, используемых программистом вручную,
только на 15-30%.
Существуют такие понятия как высокоуровневая и низкоуровневая оптимизация.
Высокоуровневые оптимизации в большинстве проводятся программистом, который,
оперируя абстрактными сущностями (функциями, процедурами, классами и т.д.) и
представляя себе общую модель решения задачи, может оптимизировать дизайн системы.
Оптимизации на уровне элементарных структурных блоков исходного кода (циклов,
ветвлений и т.д.) тоже обычно относят к высокому уровню; некоторые выделяют их в
отдельный ("средний") уровень (Н. Вирт). Низкоуровневая оптимизация производится на
этапе превращения исходного кода в набор машинных команд, и зачастую именно этот этап
подвергается автоматизации. Впрочем, программисты на ассемблере считают, что никакая
машина не превзойдет в этом хорошего программиста (при этом все согласны, что плохой
программист сделает еще хуже машины) [5].
При оптимизации кода вручную существует проблема: нужно знать не только, каким
образом проводить оптимизацию, но и в каком месте её применить. Обычно из-за разных
6
факторов (медленные операции ввода, разница в скорости работы человека-оператора и
машины и т.д.) лишь 10% кода занимают целых 90% времени выполнения. Так как на
оптимизацию придется расходовать дополнительное время, то вместо попыток оптимизации
всей программы лучше будет оптимизировать эти "критичные" ко времени выполнения 10%.
Такой фрагмент кода называют узким местом или «бутылочным горлышком» (bottleneck), и
для его определения используют специальные программы - профайлеры, которые позволяют
замерять время работы различных частей программы [4, c.5].
Рано начатая оптимизация кода ведет к усложнению и замедлению процесса
разработки, поэтому большинство советов по улучшению кода лучше применять уже на
завершающей фазе разработки, когда уже все отлажено и работает.
Главный недостаток преждевременной оптимизации - отсутствие перспективы. Это
сказывается на быстродействии итогового кода, других, еще более важных атрибутах
производительности и качестве программы. Если время, сэкономленное благодаря
реализации наиболее простой программы, посвятит ее последующей оптимизации, итоговая
программа непременно будет работать быстрее, чем программа, разработанная с
использованием неорганизационного подхода к оптимизации.
Иногда оптимизация программы после ее написания не позволяет достичь нужных
показателей производительности, из-за чего приходится вносить крупные изменения в
завершенный код. Это значит, что оптимизация небольших фрагментов все равно не привела
бы к нужным результатам. Проблема в таких ситуациях объясняется не низким качеством
кода, а неподходящей архитектурой программы.
Подход выполнения оптимизации по мере написания кода, имеет массу недостатков:
•
До создания полностью работоспособной программы найти узкие места в коде
почти невозможно. Очень трудно догадаться, на какой участок кода приходится 50%
времени выполнения, поэтому, оптимизируя код по мере написания, тратиться много
времени на оптимизацию кода, который не нуждается в ней. А на оптимизацию понастоящему важных участков времени не остается.
•
В тех случаях, когда удается определить правильно узкие места, им уделяется
слишком больше внимание, это может привести к появлению других узких мест. Если
оптимизация выполняется после создания полной системы, разработчики могут определить
все проблемные области и их относительную важность, что способствует эффективному
распределению времени.
•
Корректность,
сокрытие
информации,
удобочитаемость
становятся
вторичными целями, хотя улучшить их потом сложнее, чем производительность.
7
Если оптимизацию нужно выполнить до создания полной программы, следует
интегрировать процесс оптимизации в перспективу. Один из способов — это сделать, задать
целевые показатели объема и быстродействия отдельных функций и провести оптимизация
кода по мере его написания.
В некоторых проектах быстродействие или компактность кода действительно имеет
большое значение. Однако таких проектов немного. В таких проектах проблемы с
производительностью нужно решать путем предварительного проектирования. В остальных
случаях ранняя оптимизация представляет серьезную угрозу для общего качества ПО,
включая производительность.
Иногда методики оптимизации кода характеризуют как «практические правила» или
приводят данные, говорящие о том, что определенный вид оптимизации обязательно
приведет к желаемому результату. Однако, концепция «практических правил» плохо
описывает саму оптимизацию кода. Единственным верным правилом является оценка
результатов каждого вида оптимизации в конкретной среде. Важно убеждаться в том, что
изменение, внесенное в код, не ухудшило работу программы в целом. Так как оно может
привести к совершенно разным результатам в разных средах разработки.
3
ПОДХОД К ОПТИМИЗАЦИИ ПРОГРАММНОГО КОДА
Рассматривая целесообразность оптимизации кода, надо придерживаться следующего
алгоритма [3, c.591]:
1.
Написать хороший и понятный код, поддающийся легкому изменению
2.
Если производительность не устраивает:
a.
Сохранить работоспособную версию кода, чтобы позднее можно было
вернуться к «последнему нормальному состоянию»
b.
Оценить производительность системы с целью нахождения горячих точек
c.
Выяснить, обусловлено ли плохое быстродействие неадекватным проектом,
неверными типами данных или неудачным алгоритмами и определить, уместна ли
оптимизация кода, если оптимизация кода неуместна, вернуться к п.1
d.
Оптимизировать узкое место, определенное на этапе (с)
e.
Оценить каждое улучшение.
f.
Если оптимизация не привела к улучшению кода, вернуться к коду,
сохраненному на этапе (а) (как правило, более чем в половине случаев попытки оптимизации
будут приводить лишь к незначительному повышению производительности или к ее
снижению)
3.
Повторить процесс, начиная с п.2.
8
Исходя и вышесказанного, можно назвать несколько причин, по которым
производительность не следует повышать путем оптимизации кода. Если программа должна
быть универсальной, то нужно помнить, что методики, повышения производительности в
одной среде, могут снижать ее в других. Если поменять компилятор то, возможно, новый
компилятор будет автоматически выполнять те виды оптимизации и все усилия,
выполненные вручную, окажутся бесполезными.
Таким образом, не стоит забывать проводить оптимизацию кода, по возможности
применяя специализированные программные средства, но это следует делать аккуратно и с
осторожностью, а иногда и приготовиться к неожиданностям от компилятора.
4
МЕТОДИКИ ОПТИМИЗАЦИИ КОДА
Не существует настолько общих методик, что бы можно было их применить для
каждого кода. Однако ряд видов оптимизации кода можно, приспособить к конкретной
задаче [6, c.79].
Виды оптимизации, похожи на виды рефакторинга, однако, рефакторинг направлен на
улучшение внутренней структуры программы, а описанные ниже методы можно называть
«антирефакторингом». Эти изменения ухудшают внутреннюю структуру программы ради
повышения ее производительности. Если бы такие изменения не ухудшали внутреннюю
структуру, они бы не считались видами оптимизации – использование их было бы по
умолчанию и считалось бы методиками кодирования.
4.1
Логические выражения
Рассмотрим эффективное использование логических выражений.
•
Прекращение проверки сразу же после получения ответа
Например, выражение
if ( 5 < y && y < z )
Если y окажется меньше 5, то вторую проверку выполнять не нужно.
Некоторые языки поддерживают так называемую «сокращенную оценку выражений»,
при которой компилятор генерирует код, автоматически прекращающий проверку после
получения ответа.
Если выбранный язык не поддерживает сокращенную оценку, нужно избегать
операторов && и ||, используя вместо них дополнительную логику. Для сокращенной оценки
код следовало бы изменить так:
if ( 5 < y ) {
if ( y < z ) ... }
9
Принцип прекращения проверки сразу по получении ответа уместен и других случаях.
Например, исследование массива на наличие четных чисел. Можно решить эту задачу,
несколькими способами. Первый способ: пройтись по всему массиву и при нахождении
четного числа устанавливать флаг evenNumber. Цикл поиска может выглядеть так:
evenNumber = false;
for (int i=0; i< count; i++){
if ((input[i] % 2) ==0){
evenNumber = true;}
}
Этот способ не оптимален. Лучше было бы прекращать проверку после обнаружения
первого четного числа.
Пример оптимизированного кода представлен в таблице 4.1:
Таблица – 4.1 Оптимизированный цикл, выполняющий поиск числа
Пример 1
evenNumber = false;
for (int i=0; i< count; i++)
{
if ((input[i] % 2) ==0)
{
evenNumber = true;
break;
}
}
Код
•
Пример 2
evenNumber = false;
int i=0;
while ((i < count) && (evenNumber ==false))
{
if ((input[i] % 2)==0)
{
i++;
evenNumber = true;
}
}
Упорядочение проверок по частоте
Для повышения производительности рекомендуется размещать ветви, вероятность
выбора которых является наибольшей, ближе к началу. В том случае будет меньше тратиться
времени выбор требуемого варианта. Этот принцип относится к блокам case и цепочкам
оператора if [3, c.596].
•
Сравнение быстродействия похожих структур логики
В описанном выше пункте указанно, что данную оптимизацию можно выполнить и
для блоков case, и для оператора if. В зависимости от среды любой из подходов может
оказаться более выгодными. Так, например, в С# быстрее выполняется блок case. То есть без
оценки результатов в рабочей среде невозможно обойтись.
•
Замена сложных выражений на обращение к таблице
В некоторых случаях более быстрым, чем выполнение сложной логической цепи,
может оказаться просмотр таблиц. К категоризации чего-то и выполнении того или иного
действия, основанного на конкретной категории, чаще всего сводится суть сложной цепи.
10
Пример оптимизированного кода представлен в таблице 4.2:
Таблица – 4.2 Обращение к таблице
Исходный код
if ( ( a && !c ) || ( a && b && c ) ) {
answer = 1;
}
else if ( ( b && !a ) || ( a && c && !b ) )
{
answer = 2;
}
else if ( c && !a && !b ) {
answer = 3;
}
else {
answer = 0;
}
Код
Определение таблицы AnswerTable
static int AnswerTable[ 2 ][ 2 ][ 2 ] = {
// !b!c !bc b!c bc
0, 3, 2, 2, // !a
1, 2, 1, 1 // a
};
...
answer = AnswerTable[ a ][ b ][ c ];
4.2
Использование выражение
Большинство задач программирования нуждаются в применении математических и
логических выражений. Сложные выражения обычно дороги, но есть способы их
удешевления.
•
Алгебраические тождества
Алгебраические тождества не редко позволяют заменить «дорогие» операции на более
«дешевые». Так, следующие выражения логически эквивалентны:
not a and not b
not (a or b)
Выбор второго выражение вместо первого, экономит одну операцию not [7, c 34].
Избавление от одной операции not, не приведет к заметным результатам, но тем не
менее этот принцип значительно полезен. Джон Бентли отмечает, что в одной программе
проверялось условие sqrt(x) < sqrt(y) (Bentley, 1982). Так как sqrt(x) меньше sqrt(y), только
когда x меньше, чем y, исходную проверку можно заменить на x < y. С учетом дороговизны
метода sqrt(), можно сказать, что достигнута существенная экономии.
•
Снижение стоимости операции
Как уже было сказано, снижение стоимости операций подразумевает замену дорогой
операции более дешевой. Вот некоторые возможные варианты:
замена умножения сложением;
замена возведения в степень умножением;
замена тригонометрических функций их эквивалентами;
11
замена типа long long на long или int (следите при этом за аспектами
производительности,
связанными
с
применением
целых
чисел
естественной
и
неестественной длины);
замена чисел с плавающей запятой числами с фиксированной точкой или целые
числа;
замена чисел с плавающей запятой с удвоенной точностью числами с одинарной
точностью;
замена умножения и деления целых чисел на два операциями сдвига.
•
Инициализация во время компиляции
Если при вызове метода, передается ему в качестве единственного аргумента
именованная константа или непосредственное значение, лучше заранее вычислить нужное
значение, присвоить его константе и избежать вызова метода. Это же справедливо и для
других операций [3, c.618].
Пример вычислить значение двоичного логарифма целого числа, округлить до
ближайшего целого числа представлен в таблице 4.3.
Таблица – 4.3 Инициализация во время компиляции
Исходный код
static int Log2(int x)
{
return (int)(Math.Log(x) / Math.Log(2));
}
Код
Время
9800 тактов
выполнения
•
Недостатки системных методов
Оптимизированный код
const double LOG2 = 0.69314718;
static int Log2(int x)
{
return (int)(Math.Log(x) / LOG2);
}
6500 тактов
Системные методы очень дорогие и часто обеспечивают избыточную точность.
Зачастую такая предельная точность не нужна, не стоит тратить на нее время. Еще один
вариант оптимизации основан на том факте, что деление на 2 аналогично операции сдвига
вправо. Двоичный логарифм числа равен количеству операций деления на 2, которое можно
выполнить над этим числом до получения нулевого значения.
Пример метода, определяющего примерное значение двоичного логарифма с
использованием оператора сдвига вправо:
unsigned int Log2( unsigned int x ) {
unsigned int i = 0;
while ( ( x = ( x >> 1 ) ) != 0 ) {
i++; }
return i ; }
12
•
Использование констант корректного типа
Используйте именованные константы и литералы, имеющие тот же тип, что и
переменные, которым они присваиваются. Если константа и соответствующая ей переменная
имеют разные типы, перед присвоением константы переменной компилятор должен будет
выполнить преобразование типа.
Чуть ниже в таблице 4.4 указаны различия во времени инициализации переменных.
Таблица – 4.4 Использование констант корректного типа
Исходный код
double x;
int i;
for (int j=0; j< 10000; j++)
{
x = (double)5;
i = (int)3.14;
}
80 тактов
Код
Оптимизированный код
double x;
int i;
for (int j=0; j< 10000; j++)
{
x = 3.14;
i = 5;
}
63 тактов
Время
выполнения
•
Устранение часто используемых подвыражений
Вместо
повторяющихся несколько раз выражений, следует присвоить его значение константе и
использовать ее там, где ранее вычислялось само выражение.
Пример представлен в таблице 4.5.
Таблица – 4.5 Предварительное вычисление результатов
Код
Время
выполнения
Исходный код
int a=100;
int b=11;
double c= 12.2;
double answer = a / (1 + (c / 12) Math.Pow(1 + (c / 12), -b)) / (c / 12)
* (28 - (c / 12));
78 тактов
Оптимизированный код
int a=100;
int b=11;
double c= 5;
double d = c / 12;
double answer = a / (1 + d - Math.Pow(1
+d, -b)) / d * (28 - d);
50 тактов
4.3
Циклы
Горячие точки часто следует искать именно внутри циклов, так как они выполняются
многократно. Методики, описываемые ниже, помогут ускорить выполнение циклов [7, c.19].
•
Размыкание цикла
Если во время выполнения цикла решение не изменяется, можно разомкнуть
(unswitch) цикл, приняв решение вне цикла. Обычно для этого нужно вывернуть цикл
наизнанку, то есть поместить циклы в условный оператор, а не условный оператор внутрь
цикла. Замыканием (switching) цикла называют принятие решения внутри цикла при каждой
его итерации [3, c.602].
13
Пример представлен в таблице 4.6.
Таблица – 4.6 Размыкание цикла
Исходный код
for (int i = 0; i < 100000; i++)
{
if (sum == sumN)
{
a += array[i];
}
else
{
b += array[i];
}
}
Код
Время
1050 тактов
выполнения
•
Объединение циклов
Оптимизированный код
if (sum == sumN)
{
for (int i = 0; i < 100000; i++)
{
a += array[i]; }
}
else
{
for (int i = 0; i < 100000; i++)
{
b += array[i]; }
}
800 тактов
Бывает так, что циклы работают с один набором элементов, тогда их можно
объединить (jamming). Выгода заключается в устранении затрат, связанных с выполнением
дополнительно цикла [3, c.603].
Однако этого не стоит делать, когда нет точной информации об изменении позднее
индексов, иначе это может привести к несовместимости циклов. Прежде чем объединять
циклы, убедитесь, что это не нарушит работу остальных частей кода.
Пример представлен в таблице 4.7 размер массивов 10000.
Таблица – 4.7 Объединение циклов
Исходный код
Код
for (int i = 0; i <
a.Length; i++)
{
a[i] = i;
}
for (int i = 0; i <
a.Length; i++)
{
b[i] = i;
}
167 тактов
Время
выполнения
•
Развертывание цикла
Оптимизированный код
Неправильно
оптимизированный
код
for (int i = 0; i < a.Length; for (int i = 0; i <
i++)
a.Length; i++)
{
{
a[i] = i;
a[i] = i;
for (int j = 0; j <
b[i] = i;
a.Length; j++)
}
{
b[j] = j;
}
}
99 тактов
864494 тактов
Целью развертывания (unrolling) цикла является сокращение затрат, связанных с его
выполнением.
Пример представлен в таблице 4.8 размер массива 10000.
14
Таблица – 4.8 Развертывание цикла
Исходный код
for (int i=0; i<a.Length; i++)
{
a[i] = i;
}
Оптимизированный код
for (int i=0; i<a.Length-4; i+=5)
{
a[i] = i;
a[i + 1] = i+1;
a[i + 2] = i+2;
}
Время выполнения
70 тактов
30 тактов
Экономия времени после оптимизации составляет в среднем 50%.
Код
•
Минимизация объема работы, выполняемой внутри цикла
Как говорилось, ранее одной из методик повышения эффективности циклов является
минимизация объема работы, выполняемой внутри цикла. Если есть возможность произвести
вычисление выражения или его части вне цикла и использовать внутри цикла результат
вычисления, то следует так сделать [3, c.606].
Пример представлен в таблице 4.9 размер массивов 10000.
Таблица – 4.9 Объединение циклов
Код
Исходный код
for (int i = 0; i < a.Length; i += 5)
{
a[i] = b[i] * (((sumN * 392) /
3) % 2);
}
50 тактов
Время
выполнения
•
Сигнальные значения
Оптимизированный код
int d = (((sumN * 392) / 3) % 2);
for (int i = 0; i < a.Length; i += 5)
{
a[i] = b[i] * d;
}
27 тактов
Если цикл включает проверку сложного условия, время его выполнения часто можно
сократить, упростив проверку. В случае циклов поиска это можно сделать, использовав
сигнальное значение (sentinel value) – значение, которое располагается сразу после
окончания диапазона поиска и непременно завершает поиск. Вот пример кода улучшенная
сортировка пузырьком:
static void Sort (People [] array) //сортируем данные массива
{
People temp;
for (int i = 0; i < array.Length - 1; i++)
{
bool isSorted = true;
for (int j = array.Length - 1; j > i; j--)
{
if (array[j].age() < array[j - 1].age())
{
isSorted = false;
temp = array[j];
array[j] = array[j - 1];
15
array[j - 1] = temp;
}
}
if (isSorted)
return;
}
}
Оптимизация этого кода заключается в том, что если при текущем проходе массива
данные не были переставлены, то сортировка завершается.
При сравнении улучшенного варианта и классического были получены результаты:
a.
При случайных числах время выполнения сортировок почти не отличалось.
b.
В случае, когда в массиве содержались одинаковые данные, то улучшенная
сортировка выполнялась в 13,5 раз быстрее.
Сигнальное значение можно использовать почти в любой ситуации, требующей
выполнения линейного поиска, причем не только в массивах, но и в связных списках.
Следует внимательно выбирать сигнальные значения и с осторожностью включать их в
структуры данных.
•
Вложение более ресурсоемкого цикла в менее ресурсоемкий
Когда в программе используются вложенные циклы, стоит определить какой из них
должен быть внутренним, а какой внешним. То есть цикл с меньшим количеством итераций
должен быть внешним.
Пример представлен в таблице 4.10 в исходном коде всего 1000 итераций, а во втором
коде 804.
Таблица – 4.10 Вложение более ресурсоемкого цикла в менее ресурсоемкий
Код
Время
выполнения
Исходный код
for (int i = 0; i < 200; i++)
{
for (int j = 0; j < 4; j++)
{
sum += ans[j, i];
}
}
800 тактов
4.4
Оптимизиваронный код
for (int j = 0; j < 4; j++)
{
for (int i = 0; i < 200; i++)
{
sum += ans[j,i];
}
}
750 тактов
Методы
Одним из самых эффективных способов оптимизации кода является грамотная
декомпозиция программы на методы. Небольшие, хорошо определенные методы делают
программу компактнее, устраняя повторяющиеся фрагменты кода. Они упрощают
оптимизацию, потому что рефакторинг одного метода улучшает все методы, которые его
16
вызывают. Небольшие методы относительно легко переписывать на низкоуровнеый язык.
Объемные
хитроумные
методы
понять
сложно,
а
после
переписывания
их
на
низкоуровневом языке ассемблера это вообще невыполнимо.
Переписывание кода на низкоуровневый язык обычно положительно влияет и на
быстродействие кода, и на его объем. Типичный подход к оптимизации при помощи
низкоуровневого языка таков:
1.
Напишите все приложение на высокоуровневом языке.
2.
Выполните полное тестирование приложения и проверьте его корректность.
3.
Если
производительность
недостаточна,
выполните
профилирование
приложения с целью выявления горячих точек. Так как около 50% времени выполнения
программы обычно приходится примерно на 5% кода, горячими точками обычно будут
небольшие фрагменты программы.
4.
Перепишите несколько небольших фрагментов на низкоуровневом языке для
повышения общей производительности программы.
4.5
Изменения типов данных
Изменение типов данных может быть эффективным способом сокращения кода и
повышения его быстродействия - использование вместо чисел с плавающей запятой целые
числа. Также сложение и умножение целых чисел, как правило, выполняются быстрее, чем
аналогичные операции над числами с плавающей запятой. Например, циклы выполняются
быстрее, если индекс имеет целочисленный тип.
Пример представлен в таблице 4.11.
Таблица – 4.11 Использование вместо чисел с плавающей запятой целые числа
Исходный код
Оптимизированный
double[] a = new double[10000]; int[] a = new int[10000];
for (double i=0; i<10000; i++)
for (int i = 0; i < 10000; i++)
{
{
a[(int)i] = (i+ i) * i;
a[i] = (i + i)*i;
}
}
Время выполнения
155 тактов
98 тактов
•
Использование массивов с минимальным число измерений
Код
Использовать
массивы,
имеющие
несколько
измерений,
сложно.
Ускорить
выполнение программы можно, путем структурирования данных так, чтобы их можно было
хранить в одномерном, а не двумерном или трехмерном массиве.
Пример представлен в таблице 4.12, где numRows равно 100, numColumns = 1000.
Таблица – 4.12 Использование массивов с минимальным число измерений
Код
Исходный код
for (int i = 0; i < numRows; i++)
{
Оптимизиваронный код
for (int i = 0; i < numRows *
numColumns; i++)
17
for (int j = 0; j < numColumns; j++)
{
ans[i][j] = 0;
}
{
ans[i] = 0;
}
}
1200 тактов
Время
1000 тактов
выполнения
•
Использование дополнительных индексов
Использование
дополнительного
индекса
подразумевает
добавление
данных,
связанных с основным типом данных, которые повышают эффективность обращений к нему.
Связанные данные добавляют к основному типу или хранят в параллельной структуре.
•
Кэширование
Кэширование – это способ хранения нескольких значений, в котором значения,
используемые чаще всего, получить проще, чем значения, используемые редко. Например,
если программа читает записи с диска случайным образом, метод может хранить записи,
считываемые наиболее часто, в кэше. Метод проверяет, имеется ли запись в кэше, при
получении запроса записи. При положительном результате, запись не считывается с диска, а
возвращается прямо из памяти. Кэширование можно применять и в разных областях. Можно
кэшировать и результаты ресурсоемких вычислений, особенно если их параметры просты [3,
c.614].
Польза кэширования зависит от числа запросов кэшированной информации, от
относительной
стоимости
обращения
к
кэшированному
элементу,
создания
не
кэшированного элемента и сохранения нового элемента в кэше. Другими словами,
кэширование выгодно тогда, когда генерирование нового элемента дороже. Как любая
методики оптимизации, кэширование усложняет код и не редко бывает причиной
возникновения ошибок.
Следует отметить, что результаты конкретных видов оптимизации во многом зависят
от языка, компилятора и среды. Не оценив результатов оптимизации, невозможно сказать,
помогает она программе или вредит.
Первый вид оптимизации часто далеко не самый лучший. Обнаружив эффективный
вид оптимизации, нужно продолжать пробовать и, возможно, найдется еще более
эффективный.
Оптимизация кода – это противоречивая тема. Одни считают, что оптимизация только
ухудшает надежность и удобство сопровождения программы, поэтому выполнять ее вообще
не стоит. Другие думают, что при соблюдении должной предосторожности она приносит
пользу. Поэтому при использовании вышеперечисленных методик, надо быть внимательным
и осторожным.
18
ЗАКЛЮЧЕНИЕ
В проделанной работе были решены все поставленные задачи, были рассмотрены
термин «оптимизация кода» и связанные с ним понятия, так же были изучены виды и подход
к оптимизации кода, познакомились с методиками оптимизации кода. То есть была
достигнута
поставленная
цель
данной
работы,
которая
заключалась
в
изучении
теоретических основ оптимизации программного кода.
Практическая значимость работы заключается в применении рассмотренных методик
для оптимизации кода.
Основываясь на опыте проделанной работы, можно утверждать, несмотря на то, что
техника совершенствуется с каждым годом, нельзя пренебрегать очевидными и базовыми
правилами в процессе написания программы. Информации становится больше и
обрабатывать ее надо так же хорошо, вот почему стоит уделять пристальное внимание
созданию качественного оптимального кода.
19
СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ
1
Ашманов С.А. Теория оптимизации в задачах и упражнениях / М.: Наука. Гл.
ред. физ. -мат. лит. , 1991, - 448 с.
2
Вирт Н. Построение компиляторов / Пер. с англ. Борисов Е.В., Чернышов Л.Н.
– М.:ДМК Пресс, 2010. – 192с.:ил.
3
Макконнелл С. Совершенный код. Мастер – класс / Пер. с англ – М.:
Издательство «Русская редакция», 2010. – 896 стр.:ил.
4
Дистанционный
курс
«Оптимизация»
[Электронный
ресурс].
URL:
http://www.ict.edu.ru/ft/005128/ch11.pdf (дата обращения: 25.02.2017). Загл. с экрана. Яз. рус.
5
Дистанционный курс «Оптимизация
кода»
[Электронный ресурс]. URL:
http://www.viva64.com/ru/t/0084/ (дата обращения: 15.11.2016). Загл. с экрана. Яз. рус.
6
Касьянов В.Н. Оптимизирующие преобразования программ / Москва:«Наука»,
Главная редакция физико-математической литературы, 1988. 336 с.
7
Савельев В.А. Распараллеливание программ: учебник / Штейнберг Б.Я. -
Ростов-на-Дону: Издательство ЮФУ, 2008. - 192 с. ISBN 978-5-9275-0547-0.
20
Download