8.2.1. Вычисление констант вне цикла
Самым очевидным и самым важным правилом при создании цикла на любом языке программирования является вынос всех переменных, которые не изменяются на протяжении цикла, за его пределы. В случае ассемблера имеет смысл также по возможности разместить все переменные, которые будут использоваться внутри цикла, в регистры, а старые значения нужных после цикла регистров сохранить в стеке.
8.2.2. Перенос проверки условия в конец цикла
Циклы типа while или for, которые так часто применяются в языках высокого уровня, оказываются менее эффективными по сравнению с циклами типа until из-за того, что в них требуется лишняя команда перехода.
; for (i = start_i; i < n; i++) <тело цикла>
mov edi, start_i ; Начальное значение счётчика
mov esi, n ; Конечное значение счётчика
loop_start:
cmp edi, esi ; Пока EDI < ESI – выполнять
je loop_end
<тело цикла>
inc edi
jmp loop_start
loop_end:
; i = start_i; do { <тело цикла> } while (i < n);
mov edi, start_i
mov esi, n
loop_start:
<тело цикла>
inc edi
cmp edi, esi
jb loop_start ; Пока EDI < ESI – выполнять
Предположим, в цикле должен быть один шаг. Тогда в цикле с предусловием будет выполнено сравнение, тело цикла, безусловный переход к началу цикла, сравнение и переход за цикл. В цикле с постусловием будет выполнено тело цикла, сравнение и нереализованный переход. Таким образом, в цикле с предусловием выполняется одно лишнее сравнение и два реализованных перехода (2 * 3 такта = 6 тактов) вместо одного нереализованного (1 такт). Вроде бы и немного, но если цикл окажется внутри другого цикла, то все эти лишние такты будут повторяться многократно. Кроме того, цикл с постусловием содержит на одну команду меньше.
Конечно, цикл с постусловием всегда выполняется хотя бы один раз, и во многих случаях перед циклом приходится добавлять ещё одну проверку, но в любом случае даже небольшое уменьшение тела цикла всегда оказывается необходимой операцией.
8.2.3. Выполнение цикла задом наперёд
Циклы, в которых значение счётчика растёт от единицы или нуля до некоторой величины, можно реализовать вообще без операции сравнения, выполняя цикл в обратном направлении. Флаги меняются не только командой сравнения, но и многими другими. В частности, команда DEC меняет флаги AF, OF, PF, SF и ZF. Команда сравнения кроме этих флагов меняет также флаг CF, но для сравнения с нулём можно обойтись флагами SF и ZF.
; Цикл от 10 до 1
mov edx, 10
loop_start:
<тело цикла>
dec edx ; Уменьшаем EDX на 1. Если EDX = 0, то ZF = 1
jnz loop_start ; Переход если ZF = 0. Когда EDX = 0, ZF = 1, поэтому выходим из цикла
; Цикл от 10 до 0
mov edx, 10
loop_start:
<тело цикла>
dec edx ; Уменьшаем EDX на 1. Если EDX = -1, то SF = 1
jns loop_start ; Переход если SF = 0. Когда EDX = -1, SF = 1, поэтому выходим из цикла
Циклы от 0 и от 1 являются, наверное, самыми распространёнными. Конечно, не все циклы можно заставить выполняться в обратном направлении сразу. Например, иногда приходится изменять формат хранения массива данных также на обратный, иногда приходится вносить другие изменения, но в целом, если это возможно, всегда следует стремиться к циклам, выполняющимся задом наперёд.
8.2.4. Разворачивание циклов
Для небольших циклов время выполнения проверки условия и перехода на начало цикла может оказаться значительным по сравнению со временем выполнения самого тела цикла. В таких случаях можно вообще не создавать цикл, а просто повторить его тело нужное число раз (разумеется, только в случае, если нам заранее известно это число!). Для очень коротких циклов можно, например, удваивать или утраивать тело цикла, если, конечно, число повторений кратно двум или трём. Кроме того, бывает удобно часть работы сделать в цикле, а часть развернуть.
; Цикл от 10 до -1
mov edx, 10
loop_start:
<тело цикла>
dec edx
jns loop_start ; Выходим из цикла, когда EDX станет равны -1
<тело цикла> ; Но повторяем тело цикла ещё раз
Естественно, эти простые методики не перечисляют все возможности оптимизации среднего уровня, более того, они не описывают и десятой доли всех её возможностей. Умение оптимизировать программы нельзя сформулировать в виде набора простых алгоритмов – слишком много существует различных ситуаций, в которых всякий алгоритм оказывается неоптимальным. При решении любой задачи оптимизации приходится пробовать десятки различных небольших изменений, далеко не все из которых оказываются полезными. Именно потому, что оптимизация всегда занимает очень много времени, рекомендуется приступать к ней только после того, как программа окончательно написана.
8.3. Низкоуровневая оптимизация
8.3.1. Основные принципы
Так как современные процессоры используют весьма сложный набор команд, большинство операций можно выполнить на низком уровне очень многими способами. При этом иногда оказывается, что наиболее очевидный способ – не самый быстрый. Часто простыми перестановками команд, зная механизм выполнения команд на современных процессорах, можно заставить ту же процедуру выполняться на 50–200% быстрее. Разумеется, переходить к этому уровню оптимизации можно только после того, как текст программы окончательно написан и максимально оптимизирован на среднем уровне.
Перечислим основные рекомендации.
Используйте регистр ЕАХ всюду, где возможно. Команды с непосредственным операндом, с операндом – абсолютным адресом переменной и команды XCHG с регистрами занимают на один байт меньше, если другой операнд – регистр ЕАХ.
Если к переменной в памяти, адресуемой со смещением, выполняется несколько обращений – загрузите её в регистр.
Не используйте сложные команды – ENTER, LEAVE, LOOP, строковые команды, если аналогичное действие можно выполнить небольшой последовательностью простых команд.
Не используйте умножение или деление на константу – его можно заменить другими командами (см. раздел 6.3).
Старайтесь программировать условия и переходы так, чтобы переход выполнялся по менее вероятному событию.
Следующее эмпирическое правило, относящееся к переходам и вызовам, очень простое: избавляться от них везде, где только можно. Для этого организуйте программу так, чтобы она исполнялась прямым, последовательным образом, с минимальным числом точек принятия решения. В результате очередь команд будет почти всегда заполнена, а вашу программу будет легче читать, сопровождать и отлаживать. Процедуры, особенно небольшие, нужно не вызывать, а встраивать. Это, конечно, увеличивает размер программы, но даёт существенный выигрыш во времени её исполнения.
Используйте короткую форму команды JMP, где возможно (jmp short <метка>).
Команда LEA быстро выполняется и имеет много неожиданных применений (см. раздел 8.3.2).
Многие одиночные команды, как это ни странно, выполняются дольше, чем две или три команды, приводящие к тому же результату. Это может быть связано с различными особенностями выполнения команд, в том числе, с возможностью/невозможность попарного выполнения команд в разных конвейерах (см. раздел 8.3.3).
Старайтесь выравнивать данные и метки по адресам, кратным 2/4/8/16 (см. раздел 8.3.4).
Если команда обращается к 32-битному регистру, например ЕАХ, сразу после команды, выполнявшей запись в соответствующий частичный регистр (АХ, AL, АН), может происходить пауза в один или несколько тактов.
8.3.2. Использование команды LEA
Команда LEA может использоваться для трёхоперандного сложения (но только сложения, а не вычитания).
lea eax, [ebx + edx]
Команда LEA может использоваться для сложения значения регистра с константой или вычитания константы из значения регистра. В данном случае вычитание возможно, т. к. оно рассматривается как сложение с отрицательной константой. Результат может быть помещён в тот же или другой регистр (кроме регистра ESP). Такой способ используется для сохранения флагов, т. к. команда LEA, в отличие от команд ADD, SUB, INC и DEC, не меняет флаги.
lea eax, [eax + 1] ; Сохраняем флаги
lea eax, [ebx – 4]
Команда LEA может использоваться для быстрого умножения на константы 2, 3, 4, 5, 7(?), 8, 9. Адрес, загружаемый командой LEA, может быть суммой двух регистров, один из которых может быть умножен на константу 2, 4 или 8. Поэтому комбинируя умножение и сложение можно получить вышеперечисленные константы. Третье слагаемое может быть константой.
lea eax, [eax * 4 + eax] ; EAX = EAX * 5
lea eax, [ebx * 8 + ecx – 32]
8.3.3. Замена команд
Вместо команды AND лучше использовать команду TEST, если нужен не результат, а проверка. Команда TEST лучше спаривается. Команда TEST также может быть использована для проверки на равенство нулю.
test eax, eax
jz <метка> ; Переход, если EAX = 0
Если за командой CALL сразу же следует команда RET, замените эти команды командой JMP. Вызываемая процедура осуществит возврат по адресу возврата, переданному вызывающей процедуре.
|
Из за большого объема этот материал размещен на нескольких страницах:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |


