Магда Юрий
Шрифт:
Ранее мы уже достаточно подробно анализировали исходный текст процедуры find num, поэтому остановимся лишь на последних изменениях (они выделены жирным шрифтом). Как видно из листинга, в случае равенства переменных д50 и 1100 команда emove EAX, [ESI] копирует искомое значение в регистр ЕАХ. Следующая инструкция je exit передает управление либо на выход процедуры (флаг ZF = 1), либо следующей команде (в данном случае add ESI, 4). Команда emove EAX, [ESI] не влияет на состояние флагов, поэтому инструкция je exit, фактически, анализирует флаги, установленные командой emp a1, 1100. Как видите, в модифицированном варианте процедуры осталось всего две команды условных переходов.
Более-менее сложная программа, помимо условных и безусловных переходов, может содержать один или несколько вычислительных циклов. Вычисления или другие операции, выполняющиеся циклически, могут повторяться от нескольких десятков до сотен миллионов раз, создавая ощутимую нагрузку на память и процессор. Правильная организация циклов помогает не только повысить производительность программы в целом, но и снизить потребление ресурсов. Сейчас мы проанализируем, каким образом можно повысить производительность программы или процедуры на ассемблере за счет оптимизации циклических вычислений.
Листинг 5.8. Поиск элемента массива, находящегося в диапазоне 50-100 (окончательный вариант)
Вначале остановимся на теоретическом аспекте проблемы. В наиболее общем виде цикл представляет собой последовательность команд, начинающихся с определенной метки и возвращающихся на нее после выполнения этой последовательности. В псевдокодах ассемблера цикл можно представить так:
Например, следующая последовательность команд представляет собой простой цикл:
В этом цикле выполняется увеличение содержимого регистра ЕВХ от 0 до 100 000, и при достижении этого значения передается управление на метку exit.
Проанализируем этот фрагмент кода с точки зрения быстродействия. Его нельзя назвать оптимальным, поскольку для анализа условия выхода из цикла и самого выхода из цикла используется несколько команд переходов. Для цикла с фиксированным числом итераций проблему оптимизации решить несложно, как показано в следующем фрагменте кода:
Как видно из этого листинга, значение счетчика цикла помещается в регистр EDX. Этот фрагмент более эффективен, чем предыдущий. Команда декремента устанавливает флаг ZF, когда счетчик становится равным 0, что приводит к выходу из цикла, иначе цикл продолжается. Этот фрагмент кода требует меньшего количества команд и будет выполняться быстрее.
Еще один способ повышения быстродействия циклических вычислений состоит в том, чтобы по возможности избавиться от ветвлений и условных переходов внутри самого цикла. Рассмотрим пример (листинг 5.9). Пусть в массиве целых чисел необходимо заменить все отрицательные числа нулями. Это можно сделать с помощью 32-разрядной процедуры на ассемблере (назовем ее setO).
Листинг 5.9. Замена отрицательных элементов массива целых чисел нулевыми значениями
В этой процедуре выполняется основной цикл, операторы которого находятся между меткой next и инструкцией jnz next и образуют тело цикла. В теле цикла присутствует команда jge nochange, выполняющая ветвление в зависимости от результата сравнения. Подобные изменения в последовательности выполнения отрицательно сказываются на быстродействии, поэтому попробуем избавиться от лишнего перехода. Сделать это можно с помощью команды setge. Посмотрим, как будет выглядеть модифицированный вариант процедуры (листинг 5.10).
Листинг 5.10. Модифицированный вариант листинга 5.9, в котором используется команда setge
Хочу отметить, что даже хорошо оптимизированный цикл иногда не работает так быстро, как ожидает разработчик. Для дальнейшего повышения эффективности прибегают к так называемому разворачиванию (unrolling) цикла. Этот термин означает на самом деле, что цикл должен выполнять больше действий в одной итерации для уменьшения количества итераций. Это дает неплохой эффект, и сейчас мы рассмотрим два фрагмента программного кода, в которых имеет место разворачивание циклов.
В качестве исходного (неоптимизированного) фрагмента кода возьмем, например, копирование данных размером в двойное слово из одного буфера памяти (обозначим его как src) в другой (dst). Исходный текст представлен в листинге 5.11.
Листинг 5.11. Копирование двойных слов без оптимизации
Для разворачивания цикла выполним одновременно копирование двух двойных слов. Исходный текст оптимизированного фрагмента кода показан в листинге 5.12 (изменения выделены жирным шрифтом).