Примеры применения команд в режиме THUMB
В нижеследующих примерах продемонстрированы различные способы использования THUMB команд с целью получения эффективного и малого по размеру кода. В каждом примере для сравнения приведен эквивалент его реализации в ARM командах.
Умножение на константу, используя только операции сдвига и умножения
Ниже приведен пример кода для умножения на различные константы, использую одну, две или три THUMB команды (рядом приведен аналогичный пример при использовании ARM команд). Для других констант лучше использовать встроенную команду аппаратного умножения MUL взамен последовательности из 4-х обычных команд.
Thumb ARM
1. Умножение на 2^n (1,2,4,8,...)
LSL Ra, Rb, LSL #n MOV Ra, Rb, LSL #n
2. Умножение на 2^n+1 (3,5,9,17,...)
LSL Rt, Rb, #n ADD Ra, Rb, Rb, LSL #n
ADD Ra, Rt, Rb
3. Умножение на 2^n-1 (3,7,15,...)
LSL Rt, Rb, #n RSB Ra, Rb, Rb, LSL #n
SUB Ra, Rt, Rb
4. Умножение на -2^n (-2, -4, -8, ...)
LSL Ra, Rb, #n MOV Ra, Rb, LSL #n
MVN Ra, Ra RSB Ra, Ra, #0
5. Умножение на -2^n-1 (-3, -7, -15, ...)
LSL Rt, Rb, #n SUB Ra, Rb, Rb, LSL #n
SUB Ra, Rb, Rt
6. Умножение на любое число из C = {2^n+1, 2^n-1, -2^n or -2^n-1} * 2^n
Эффективна последовательность из 2…5 следующих друг за другом операций сдвига.
Это позволяет производить умножение на следующие константы:
6, 10, 12, 14, 18, 20, 24, 28, 30, 34, 36, 40, 48, 56, 60, 62 .....
(2..5) (2..5)
LSL Ra, Ra, #n MOV Ra, Ra, LSL #n
|
Классическое деление со знаком
Ниже приведенные примеры демонстрируют классическую операцию деления со знаком и получением остатка от деления в обоих режимах THUMB и ARM.
THUMB код
signed_divide
; Деление со знаком содержимого регистра R1 на R0: результат в R0, остаток в R1
; Получить абсолютное значение R0 и разместить его в R3
ASR R2, R0, #31 ; Получить 0 или -1 в R2 в зависимости от знака R0
EOR R0, R2 ; EOR с -1 (0xFFFFFFFF), если минус
SUB R3, R0, R2 ; и выполнить ADD 1 (SUB -1) для получения asb значения
; SUB всегда влияет на флаги CPSR:
BEQ divide_by_zero ; если деление на ноль, то соообщить об этом.
; Получить asb значение содержимого R1 выполняя EOR с 0xFFFFFFFF
; и дополнить единицей, если минус
ASR R0, R1, #31 ; Получить 0 или -1 в R3 в зависимости от знака R1
EOR R1, R0 ; EOR с -1 (0xFFFFFFFF), если минус
SUB R1, R0 ; и выполнить ADD 1 (SUB -1) для получения asb значения
; Сохранить знаки (0 или -1 в R0 & R2) для последующего использования
PUSH {R0, R2}
; Выравнивание, сдвиг вправо на один бит до тех пор, пока делитель (содержимое R0)
; меньше или равно, чем делимое (содержимое R1).
LSR R0, R1, #1
MOV R2, R3
B %FT0
just_l
LSL R2, #1
0 CMP R2, R0
BLS just_l
MOV R0, #0 ; Очистить R0
B %FT0 ; Перейти к циклу деления
div_l
LSR R2, #1
0 CMP R1, R2 ; R1 > R2 ?
BCC %FT0
SUB R1, R2 ; Выполнить обычное вычитание
0 ADC R0, R0 ; Сдвиг результат и сложение с единицей
CMP R2, R3 ; Прервать, когда R2 равно R3
BNE div_l
; Откорректировать знак результата (R0) и остатка от деления (R1)
POP {R2, R3} ; Извлечь из стека знаки делимого и делителя
EOR R3, R2 ; Знак результата
EOR R0, R3 ; Минус, если знак результата равен -1
SUB R0, R3
EOR R1, R2 ; Инверсия остатка от деления, если знак делимого равен -1
SUB R1, R2
MOV pc, lr
|
ARM код
signed_divide
; effectively zero a4 as top bit will be shifted out later
; Обнулить a4, поскольку старший бит будет сдивнут позже
ANDS a4, a1, #&80000000
RSBMI a1, a1, #0
EORS ip, a4, a2, ASR #32
; ip бит 31 = знак результата
; ip bit 30 = знак a2
RSBCS a2, a2, #0
; центральная часть кода идентична коду безнакого деления (udiv)
; (без выполнения MOV a4, #0)
MOVS a3, a1
BEQ divide_by_zero
just_l
; На стадия выравнивания происходит единовременный сдвиг 1 бита
CMP a3, a2, LSR #1
MOVLS a3, a3, LSL #1
; NB: LSL #1 всегда - OK, если LS выполнена
BLO s_loop
div_l
CMP a2, a3
ADC a4, a4, a4
SUBCS a2, a2, a3
TEQ a3, a1
MOVNE a3, a3, LSR #1
BNE s_loop2
MOV a1, a4
MOVS ip, ip, ASL #1
RSBCS a1, a1, #0
RSBMI a2, a2, #0
MOV pc, lr
|
Деление на константу
Система команд ARM была разработана с соответствии с RISC архитектурой. В результате этого ARM ядро не имеет команд аппаратного деления, поэтому деление в приложении должно выполняться программно. Но программное деление будет выполнять значительно медленнее, чем аппаратное деление. Однако, зачастую это не так критично для большинства приложений.
Очевидно, что деление-на-константу выполняется значительно быстрее, чем обычное деление. Если в приложении это допустимо, то лучше использовать такое деление. В примере div.c подемонстрирован процесс деления на константу для системы команд ARM.
Простейший случай: деление-на-константу 2^n - выполняется с помощью обычных команд сдвига делимого вправо. Однако, существует нюанс, который касается обработки чисел со знаком: для их деления необходимое использование арифметического сдвига вправо с целью корректного сохранения знака результата. И наоборот, для чисел без знака достаточно обычного логического сдвига вправо с заполнением вдвигаемых старших бит нулями:
MOV a2, a1, lsr #5 ; беззнаковое деление на 32
MOV a2, a1, asr #10 ; деление со знаком на 1024
|
Принцип работы деления на константу
Основой метода деления-на-константу фактически являются операции умножения:
x/y = x * (1/y)
При рассмотрении работы этого метода будут подразумеваться числа с фиксированной точкой в формате 0.32 (это означает, что 0 бит отедено под старшую часть до запятой и 32 бита после запятой) .
= (x * (2^32/y)) / 2^32 = (x * (2^32/y)) >> 32
Это возвращает старшие 32 бита 64-битного результата х и (2^32/y). Если у - константа, то (2^32/y) - тоже константа. Для каждого значения y можно задать в виде таблицы соответвующее ему (2^32/y):
y (2^32/y)
2 10000000000000000000000000000000 #
3 01010101010101010101010101010101 *
4 01000000000000000000000000000000 #
5 00110011001100110011001100110011 *
6 00101010101010101010101010101010 *
7 00100100100100100100100100100100 *
8 00100000000000000000000000000000 #
9 00011100011100011100011100011100 *
10 00011001100110011001100110011001 *
11 00010111010001011101000101110100
12 00010101010101010101010101010101 *
13 00010011101100010011101100010011
14 00010010010010010010010010010010 *
15 00010001000100010001000100010001 *
16 00010000000000000000000000000000 #
17 00001111000011110000111100001111 *
18 00001110001110001110001110001110 *
19 00001101011110010100001101011110
20 00001100110011001100110011001100 *
21 00001100001100001100001100001100 *
22 00001011101000101110100010111010
23 00001011001000010110010000101100
24 00001010101010101010101010101010 *
25 00001010001111010111000010100011
|
Примечание: знак # означает частный случай - 2^n, который реализуется по описанному выше способу; знак * означает частный случай - простые повторяющиеся последовательности.
Для повторившихся последовательностей существует остносительно простое решение - применение метода умножения-на-константу. Результат может быть вычислен за несколько команд. Подобное умножение несколько необычно: требуется возвращать результат в виде 32-битного числа, формируемого из старших 32-х бит 64-битного результата. Поэтому такой метод эффективен для получения результата с длиной не более 32-х бит.
Ниже приведен пример деления-на-десять (x - делимое):
SUB a1, x, x, lsr #2 ; a1 = x*%0.11000000000000000000000000000000
ADD a1, a1, a1, lsr #4 ; a1 = x*%0.11001100000000000000000000000000
ADD a1, a1, a1, lsr #8 ; a1 = x*%0.11001100110011000000000000000000
ADD a1, a1, a1, lsr #16 ; a1 = x*%0.11001100110011001100110011001100
MOV a1, a1, lsr #3 ; a1 = x*%0.00011001100110011001100110011001
|
|