Ассемблер для процессоров Intel Pentium
Шрифт:
cmp EAX, EDX
Следующая команда помещает в регистр ЕАХ содержимое EDX, если число в EDX больше числа в ЕАХ, и оставляет содержимое ЕАХ без изменения, если число в ЕАХ больше числа в EDX:
cmovl EAX, EDX
Наконец, содержимое регистра ЕАХ помещается в регистр ЕВХ. Из этого фрагмента видно, что ветвлений и переходов нет. Перед использованием команды cmov CC необходимо проверить, поддерживается ли она данным типом процессора. Такую проверку можно выполнить с помощью команды cpund.
Рассмотрим еще один пример.
Как видно из исходного текста, после команды стр программный код разветвляется. Этого можно легко избежать, если использовать команду cmovl. Более быстродействующий код выглядит так:
В нашем последнем примере представлен окончательный вариант процедуры find num, в которой используются команды set CC и cmovCC (листинг 5.8).
Ранее мы уже достаточно подробно анализировали исходный текст процедуры 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 (изменения выделены жирным шрифтом).