[Назад] [Далее] Электронные издания на русском языке

9.2.1. Оптимизация циклов

Вычисление констант вне цикла

Самым очевидным и самым важным правилом при создании цикла на любом языке программирования является вынос всех переменных, которые не изменяются на протяжении цикла, за его пределы. В случае ассемблера имеет смысл также по возможности разместить все переменные, которые будут использоваться внутри цикла, в регистры, а старые значения нужных после цикла регистров сохранить в стеке.

Перенос проверки условия в конец цикла

Циклы типа WHILE или FOR, которые так часто применяются в языках высокого уровня, оказываются менее эффективными по сравнению с циклами типа UNTIL из-за того, что в них требуется лишняя команда перехода:

; цикл типа WHILE
        mov        si,counter          ;  число повторов
        mov        dx,start_i          ; начальное значение
loop_start:
        cmp        dx,si               ; пока dx < si - выполнять
        jbn        exit_loop

;       [тело цикла]

        inc        dx
        jmp        loop_start

; почти такой же цикл типа UNTIL
        mov        si,counter
        mov        dx,start_i
loop_start:                            ; выполнять

;       [тело цикла]

        inc        dx
        cmp        dx,si               ; пока dx < si
        jb         loop_start

Естественно, цикл типа UNTIL, в отличие от цикла типа WHILE, выполнится по крайней мере один раз, так что, если это нежелательно, придется добавить одну проверку перед телом цикла, но в любом случае даже небольшое уменьшение тела цикла всегда оказывается необходимой операцией.

Выполнение цикла задом наперед

Циклы, в которых значение счетчика растет от двойки, единицы или нуля до некоторой константы, можно реализовать вообще без операции сравнения, выполняя цикл в обратном направлении (и мы пользовались этим приемом неоднократно в наших примерах). Дело в том, что команда DEC counter устанавливает флаги точно так же, как и команда СМР counter,1, то есть следующая команда условного перехода будет обрабатывать результат сравнения счетчика с единицей:

; цикл от 10 до 2
        mov        dx,10
loop_start:

;       [тело цикла]

        dec        dx               ; уменьшить DX,
        ja         loop_start       ; если DX больше 1 - продолжить цикл

; цикл от 10 до 1
        mov        dx,10
loop_start:

;       [тело цикла]

        dec        dx               ; уменьшить DX,
        jae        loop_start       ; если DX больше или равно 1 - продолжить цикл

;   цикл от 10 до 0
        mov        dx,10
loop_start:

;       [тело цикла]

        dec         dx              ; уменьшить DX,
        jns         loop_start      ; если DX не отрицательный - продолжить цикл

Конечно, не все циклы можно заставить выполняться в обратном направлении сразу. Например, иногда приходится изменять формат хранения массива данных также на обратный, иногда приходится вносить другие изменения, но в целом, если это возможно, всегда следует стремиться к циклам, выполняющимся задом наперед. Кроме того, если цикл построен в этой манере, выполняется до значения счетчика, равного нулю, и регистр СХ можно освободить для выполнения роли счетчика, есть вариант воспользоваться командой LOOP, хотя в некоторых случаях в низкоуровневой оптимизации команды DEC/JNZ оказываются более эффективными.

Разворачивание циклов

Для небольших циклов время выполнения проверки условия и перехода на начало цикла может оказаться значительным по сравнению с временем выполнения самого тела цикла. Более того, на Pentium Pro/Pentium II цикл не в состоянии выполняться меньше, чем за два такта процессора, хотя его тело может выполняться даже меньше, чем за такт. С этим легко справиться, вообще не создавая цикл, а просто повторив его тело нужное число раз (разумеется, только в случае, если нам заранее известно это число!). Для очень коротких циклов можно, например, удваивать или утраивать тело цикла, если, конечно, число повторений кратно двум или трем. Кроме того, бывает удобно часть работы сделать в цикле, а часть развернуть, например продолжая цепочку циклов из предыдущего примера:

; цикл от 10 до -1
        mov        dx,10
loop_start:

;       [тело цикла]

        dec        dx               ; уменьшить DX,
        jns        loop_start       ; если DX не отрицательный -
                                    ; продолжить цикл
;       [тело цикла]

Совершенно естественно, что эти простые методики не перечисляют все возможности оптимизации среднего уровня, более того, они не описывают и десятой доли всех ее возможностей. Умение оптимизировать программы нельзя сформулировать в виде набора простых алгоритмов — слишком много существует различных ситуаций, в которых всякий алгоритм оказывается неоптимальным. При решении любой задачи оптимизации приходится пробовать десятки различных небольших изменений, далеко не все из которых оказываются полезными. Именно потому, что оптимизация всегда занимает очень много времени, рекомендуется приступать к ней только после того, как программа окончательно написана. Как и во многих других ситуациях, с оптимизацией нельзя торопиться, но и нельзя совсем забывать о ней на любой стадии создания программы.