8-900-374-94-44
[email protected]
Slide Image
Меню

Умножение и деление ассемблер: Деление и умножение в Assembler

Содержание

Деление и умножение в Assembler

Здравствуйте, уважаемые друзья! Продолжаем изучать нашу рубрику, на очереди тема умножения и деления в Assembler. Разберемся со всеми тонкостями этих операций, конечно же, на практическом примере.

Основные команды

  • Для умножения в Assembler используют команду mul
  • Для деления в Assembler используют команду div

Правила умножения в Assembler

Итак, как мы уже сказали, при умножении и делении в Assembler есть некоторые тонкости, о которых дальше и пойдет речь. Тонкости эти состоят в том, что от того, какой размерности регистр мы делим или умножаем многое зависит. Вот примеры:

  • Если аргументом команды mul является 1-байтовый регистр (например mul bl), то значение этого регистра bl умножится на значение регистра al, а результат запишется в регистр ax, и так будет всегда, независимо от того, какой 1-байтовый регистр мы возьмем.
    bl*al = ax
  • Если аргументом является регистр из 2 байт(напримерmul bx), то значение в регистре bx умножится на значение, хранящееся в регистре ax, а результат умножения запишется в регистр eax.
    bx*ax = eax
  • Если аргументом является регистр из 4 байт(напримерmul ebx), то значение в регистре ebx умножится на значение, хранящееся в регистре eax, а результат умножения запишется в 2 регистра: edx и eax.
    ebx*eax = edx:eax

Правила деления в Assembler

Почти аналогично реализуется и деление, вот примеры:

  • Если аргументом команды div является 1-байтовый регистр (например div bl), то значение регистра ax поделится на значение регистра bl, результат от деления запишется в регистр al, а остаток запишется в регистр ah.
    ax/bl = al, ah
  • Если аргументом является регистр из 2 байт(напримерdiv bx), то процессор поделит число, старшие биты которого хранит регистр dx, а младшие ax на значение, хранящееся в регистре bx. Результат от деления запишется в регистр ax, а остаток запишется в регистр dx.
    (dx,ax)/bx = ax, dx
  • Если же аргументом является регистр из 4 байт(напримерdiv ebx), то процессор аналогично предыдущему варианту поделит число, старшие биты которого хранит регистр edx, а младшие eax на значение, хранящееся в регистре ebx. Результат от деления запишется в регистр eax, а остаток запишется в регистр edx.
    (edx,eax)/ebx = eax, edx

Программа

Далее перейдем к примеру: он не должен вызвать у вас каких либо затруднений, если вы читали наши предыдущие статьи, особенно важна статья про вывод на экран, советую вам с ней ознакомиться. Ну а мы начнем:

.386
.model flat,stdcall
option casemap:none
include ..\INCLUDE\kernel32.inc 
include ..\INCLUDE\user32.inc 
includelib ..\LIB\kernel32.lib 
includelib ..\LIB\user32.lib 
BSIZE equ 15   

.data
ifmt db "%d", 0     ;строка формата
stdout dd ?         
cWritten dd ?
CRLF WORD ?

.data?
buf db BSIZE dup(?) ;буфер

Стандартное начало, в котором мы подключаем нужные нам библиотеки и объявляем переменные для вывода чисел на экран. Единственное о чем нужно сказать: новый для нас раздел .data? Знак вопроса говорит о том, что память будет выделяться на этапе компилирования и не будет выделяться в самом исполняемом файле с расширением .exe (представьте если бы буфер был большего размера) . Такое объявление — грамотное с точки зрения программирования.

.code
start:
invoke GetStdHandle, -11 
mov stdout,eax 
mov CRLF, 0d0ah

;-------------------------деление

mov eax, 99
mov edx, 0
mov ebx, 3
div ebx
invoke wsprintf, ADDR buf, ADDR ifmt, eax
invoke WriteConsoleA, stdout, ADDR buf, BSIZE, ADDR cWritten, 0
invoke WriteConsoleA, stdout, ADDR CRLF, 2, ADDR cWritten,0

В разделе кода, уже по традиции, считываем дескриптор экрана для вывода и задаем значения для перевода каретки. Затем помещаем в регистры соответствующие значения и выполняем деление регистра ebx, как оно реализуется описано чуть выше. Думаю, тут понятно, что мы просто делим число 99 на 3, что получилось в итоге выводим на экран консоли.

;-------------------------умножение
mov bx, 4
mov ax, 3
mul bx
invoke wsprintf, ADDR buf, ADDR ifmt, eax
invoke WriteConsoleA, stdout, ADDR buf, BSIZE, ADDR cWritten, 0
invoke ExitProcess,0  
end start

Думаю, что здесь тоже все понятно и без комментариев. Как производиться умножение в Assembler вы тоже можете прочитать чуть выше, ну и результат выводим на экран.

Просмотр консоли

Этот код я поместил в файл seventh.asm, сам файл поместил в папку BIN (она появляется при установке MASM32). Далее открыл консоль, как и всегда, с помощью команды cd перешел в эту папку и прописал amake.bat seventh. Скомпилировалось, затем запускаю исполняемый файл и в консоли получаются такие числа:

Как видите, мы правильно посчитали эти операции.

На этом сегодня все! Надеюсь вы научились выполнять деление и умножение на Assembler.

Скачать исходники

Поделиться ссылкой:

Похожее

Решение: Умножение/деление со сдвигом — Assembler ✔

LOCALS
 
.model small
 
.stack 100h
 
.data
        CrLf            db      0Dh, 0Ah, '$'
        PromptGetX      db      'Input X
: ', '$'
        PromptGetY      db      'Input Y: ', '$'
        KeyBuf          db      7, 0, 8 dup(0)      
 
;max,len,string,CR(0dh)
        msgError        db      'Ошибка ввода числа', 0Dh, 0Ah, '$'
 
        X               db      ?
        Y               db      ?
        F               db      ?
.code
 
main    proc
        mov     ax,     @data
        mov     ds,     ax
 
@@GetX:
        ; ввод числа с клавиатуры (строки)
        lea     dx, PromptGetX
        mov     ah,09h
        int     21h
 
        mov     ah, 0Ah
        mov     dx, offset KeyBuf
        int     21h
 
        ; перевод строки (на новую строку)
        lea     dx, CrLf
        mov     ah,09h
        int     21h
 
        ; преобразование строки в число
        lea     si, KeyBuf+1
        lea     di, X
        call    Str2Num
 
        ; проверка на ошибку
        jnc     @@GetY
 
        ; если есть ошибка ввода - напечатать сообщение об ошибке
        lea     dx, msgError
        mov     ah,09h
        int     21h
        jmp     @@GetX
 
@@GetY:
        ; ввод числа с клавиатуры (строки)
        lea     dx, PromptGetY
        mov     ah,09h
        int     21h
 
        mov     ah, 0Ah
        mov     dx, offset KeyBuf
        int     21h
 
        ; перевод строки (на новую строку)
        lea     dx, CrLf
        mov     ah,09h
        int     21h
 
        ; преобразование строки в число
        lea     si, KeyBuf+1
        lea     di, Y
        call    Str2Num
 
        ; проверка на ошибку
        jnc     @@Calc
 
        ; если есть ошибка ввода - напечатать сообщение об ошибке
        lea     dx, msgError
        mov     ah,09h
        int     21h
        jmp     @@GetY
 
@@Calc:
    mov ax, 0 ;bx:=96*X
    mov al, X
    mov cl, 5
    sal ax, cl
    mov bx, ax
    sal ax, 1
    add bx, ax
 
    mov cl, 4 ;bx:=(96*X)/16
    sar bx, cl
 
    mov bx, 9 ;ax:=Y
    mov al, Y
 
    sub ax, bx ;ax:=Y-(96*X)/16
     
        
        call    Show_AX
 
        mov     ax,     4C00h
        int     21h
main    endp
 
; выводит число из регистра AX на экран
; входные данные:
; ax - число для отображения
Show_AX proc
        push    ax
        push    bx
        push    cx
        push    dx
        push    di
 
        mov     cx, 10
        xor     di, di          ; di - кол.
цифр в числе           ; если число в ax отрицательное, то         ;1) напечатать '-'         ;2) сделать ax положительным         or      ax, ax         jns     @@Conv         push    ax         mov     dx, '-'         mov     ah, 2           ; ah - функция вывода символа на   экран         int     21h         pop     ax           neg     ax   @@Conv:         xor     dx, dx         div     cx              ; dl = num mod 10         add     dl, '0'         ; перевод в символьный формат         inc     di         push    dx              ; складываем в стэк         or      ax, ax         jnz     @@Conv         ; выводим из стэка на экран @@Show:         pop     dx              ; dl = очередной символ         mov     ah, 2           ; ah - функция вывода символа на   экран         int     21h         dec     di              ; повторяем пока di<>0         jnz     @@Show           pop     di         pop     dx         pop     cx         pop     bx         pop     ax         ret Show_AX endp   ; преобразования строки в число ; на входе: ; ds:[si] - строка с числом ; ds:[di] - адрес числа ; на выходе ; ds:[di] - число ; CY - флаг переноса (при ошибке - установлен, иначе - сброшен) Str2Num proc         push    ax         push    bx         push    cx         push    dx         push    ds         push    es         push    si           push    ds         pop     es           mov     cl, ds:[si]         xor     ch, ch           inc     si           mov     bx, 10         xor     ax, ax           ;если в строке первый символ '-'         ; - перейти к следующему         ; - уменьшить количество рассматриваемых символов         cmp     byte ptr [si], '-'         jne     @@Loop         inc     si         dec     cx @@Loop:         mul     bx         ; умножаем ax на 10 ( dx:ax=ax*bx )         mov     [di], ax   ; игнорируем старшее слово         cmp     dx, 0      ; проверяем, результат на переполнение         jnz     @@Error           mov     al, [si]   ; Преобразуем следующий символ в число         cmp     al, '0'         jb      @@Error         cmp     al, '9'         ja      @@Error         sub     al, '0'         xor     ah, ah         add     ax, [di]         jc      @@Error    ; Если сумма больше 65535         cmp     ax, 8000h         ja      @@Error         inc     si           loop    @@Loop           pop     si         ;проверка на знак         push    si         inc     si         cmp     byte ptr [si], '-'         jne     @@Check    ;если должно быть положительным         neg     ax         ;если должно быть отрицательным         jmp     @@StoreRes @@Check:                   ;дополнительная проверка, когда при вводе   положительного числа получили отрицательное        or       ax, ax     ;        js       @@Error @@StoreRes:                ;сохранить результат         mov     [di], ax         clc         pop     si         pop     es         pop     ds         pop     dx         pop     cx         pop     bx         pop     ax         ret @@Error:         xor     ax, ax         mov     [di], ax         stc         pop     si         pop     es         pop     ds         pop     dx         pop     cx         pop     bx         pop     ax         ret Str2Num endp   end     main

Лекция 7.

Арифметические команды языка Ассемблер: аддитивные и мультипликативные команды целочисленных операций.

7.1. Сложение и вычитание.

7.1.1. ADD – команда для сложения двух чисел. Она работает как с числами со знаком, так и без знака.

ADD Приемник, Источник

Логика работы команды:

<Приемник> = <Приемник> + <Источник>

Возможные сочетания операндов для этой команды аналогичны команде MOV.

По сути дела, это – команда сложения с присвоением, аналогичная принятой в языке C/C++:

Приемник += Источник;

Операнды должны иметь одинаковый размер. Результат помещается на место первого операнда.

После выполнения команды изменяются флаги, по которым можно определить характеристики результата:

  1. Флаг CF устанавливается, если при сложении произошёл перенос из старшего разряда.
    Для беззнаковых чисел это будет означать, что произошло переполнение и результат получился некорректным.
  2. Флаг OF обозначает переполнение для чисел со знаком.
  3. Флаг SF равен знаковому биту результата (естественно, для чисел со знаком, а для беззнаковых он равен старшему биту и особо смысла не имеет).
  4. Флаг ZF устанавливается, если результат равен 0.
  5. Флаг PF — признак чётности, равен 1, если результат содержит нечётное число единиц.

Примеры:

add ax,5     ;AX = AX + 5

add dx,cx    ;DX = DX + CX

add dx,cl    ;Ошибка: разный размер операндов.

7.1.2. SUB — команда для вычитания одного числа из другого. Она работает как с числами со знаком, так и без знака.

SUB Приемник, Источник

Логика работы команды:

<Приемник> = <Приемник> — <Источник>

Возможные сочетания операндов для этой команды аналогичны команде MOV.

По сути дела, это – команда вычитания с присвоением, аналогичная принятой в языке C/C++:

Приемник -= Источник;

Операнды должны иметь одинаковый размер. Результат помещается на место первого операнда.

На самом деле вычитание в процессоре реализовано с помощью сложения. Процессор меняет знак второго операнда на противоположный, а затем складывает два числа.

Примеры:

sub ax,13     ;AX = AX — 13

sub ax,bx    ;AX = AX + BX

sub bx,cl    ;Ошибка: разный размер операндов.

7.1.3. Инкремент и декремент. Очень часто в программах используется операция прибавления или вычитания единицы. Прибавление единицы называется инкрементом, а вычитание — декрементом. Для этих операций существуют специальные команды процессора: INC и DEC. Эти команды не изменяют значение флага

CF.

Эти команды содержит один операнд и имеет следующий синтаксис:

INC Операнд

DEC Операнд

Логика работы команд:

INC: <Операнд> = < Операнд > + 1

DEC: <Операнд> = < Операнд > — 1

В качестве инкремента допустимы регистры и память: reg, mem.

Примеры:

inc ax     ;AX = AX + 1

dec ax     ;AX = AX — 1

7.1.4. NEG – команда для изменения знака операнда.

Синтаксис:

NEG Операнд

Логика работы команды:

<Операнд> = – < Операнд >

В качестве декремента допустимы регистры и память: reg, mem.

Примеры:

neg ax       ;AX = -AX

7.2. Сложение и вычитание с переносом.

В системе команд процессоров x86 имеются специальные команды сложения и вычитания с учётом флага переноса (CF). Для сложения с учётом переноса предназначена команда ADC, а для вычитания — SBB. В общем, эти команды работают почти так же, как ADD и SUB, единственное отличие в том, что к младшему разряду первого операнда прибавляется или вычитается дополнительно значение флага CF.

Они позволяют выполнять сложение и вычитание многобайтных целых чисел, длина которых больше, чем разрядность регистров процессора (в нашем случае 16 бит). Принцип программирования таких операций очень прост — длинные числа складываются (вычитаются) по частям. Младшие разряды складываются(вычитаются) с помощью обычных команд ADD и SUB, а затем последовательно складываются(вычитаются) более старшие части с помощью команд ADC и SBB. Так как эти команды учитывают перенос из старшего разряда, то мы можем быть уверены, что ни один бит не потеряется. Этот способ похож на сложение(вычитание) десятичных чисел в столбик.

На следующем рисунке показано сложение двух двоичных чисел командой ADD:

При сложении происходит перенос из 7-го разряда в 8-й, как раз на границе между байтами. Если мы будем складывать эти числа по частям командой ADD, то перенесённый бит потеряется и в результате мы получим ошибку. К счастью, перенос из старшего разряда всегда сохраняется в флаге CF. Чтобы прибавить этот перенесённый бит, достаточно применить команду ADC:

 

Пример:

#include <iostream.h>

#include <iomanip.h>

void main()

{

  //Сложение двух чисел с учетом переноса: FFFFFFAA + FFFF

  int a, b;

  asm {

      mov eax, 0FFFFFFAAh

      mov ebx, 0FFFFh

      mov edx, 0

      mov ecx, 0

      add eax, ebx

      adc edx, ecx

      mov a, edx

      mov b, eax

   }

   cout << hex << a << setw(8) << setfill(‘0’) << b; //10000ffa9

}

7. 3. Умножение и деление.

7.3.1. MUL – команда умножения чисел без знака. У этой команды только один операнд — второй множитель, который должен находиться в регистре или в памяти. Местоположение первого множителя и результата задаётся неявно и зависит от размера операнда:

Размер операнда

Множитель

Результат

1 байт

AL

AX

2 байта

AX

DX:AX

4 байта

EAX

EDX:EAX

Отличие умножения от сложения и вычитания в том, что разрядность результата получается в 2 раза больше, чем разрядность сомножителей.

Примеры:

mul bl    ;AX = AL * BL

mul ax    ;DX:AX = AX * AX

Если старшая часть результата равна нулю, то флаги CF и ОF будут иметь нулевое значение. В этом случае старшую часть результата можно отбросить.

7.3.2. IMUL – команда умножения чисел со знаком. Эта команда имеет три формы, различающиеся количеством операндов:

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

2.       С двумя операндами — указываются два множителя. Результат записывается на место первого множителя. Старшая часть результата в этом случае игнорируется. Кстати, эта форма команды не работает с операндами размером 1 байт.

3.       С тремя операндами — указывается положение результата, первого и второго множителя. Второй множитель должен быть непосредственным значением. Результат имеет такой же размер, как первый множитель, старшая часть результата игнорируется. Это форма тоже не работает с однобайтными множителями.

Примеры:

imul cl           ;AX = AL * CL

imul bx,ax        ;BX = BX * AX

imul cx,-5        ;CX = CX * (-5)

imul dx,bx,134h   ;DX = BX * 134h

CF = OF = 0, если произведение помещается в младшей половине результата, иначе CF = OF = 1. Для второй и третьей формы команды CF = OF = 1 означает, что произошло переполнение.

7.3.3. DIV – команда деления чисел без знака. У этой команды один операнд — делитель, который должен находиться в регистре или в памяти. Местоположение делимого, частного и остатка задаётся неявно и зависит от размера операнда:

Размер операнда
(делителя)

Делимое

Частное

Остаток

1 байт

AX

AL

AH

2 байта

DX:AX

AX

DX

4 байта

EDX:EAX

EAX

EDX

При выполнении команды DIV может возникнуть прерывание (в данном курсе прерывания мы рассматривать не будем поэтому старайтесь избегать таких случаев):

  • если делитель равен нулю;
  • если частное не помещается в отведённую под него разрядную сетку (например, если при делении слова на байт частное больше 255).

Примеры:

div cl   ;AL = AX / CL, остаток в AH

div di   ;AX = DX:AX / DI, остаток в DX

7.3.4. IDIV – команда деления чисел со знаком. Единственным операндом является делитель. Местоположение делимого и частного определяется также, как для команды DIV. Эта команда тоже генерирует прерывание при делении на ноль или слишком большом частном.

7.3.5. NOP – ничего не делающая команда.

Синтаксис:

NOP

Примеры:

nop

 

Пример. (5 + 8) / (2 * 3)

#include <iostream.h>

void main()

{

   asm {

        mov bx, 5 //BL = 5

      add bx, 8 //BL = BL + 8  |  13

      sub bx, 1 //BL = BL — 1  |  12

      mov al, 2 //AL = 2

      mov cl, 3 //CL = 3

      mul cl    //AX = AL * CL  |  6

      //AX = 6, BL = 12

      xchg bx, ax //AX = 12, BX = 6

      mov dx, 0

      div bx

   }

}

 

Основные понятия

Цель работы: изучение операций сложения, вычитания, умножения и деления двоичных чисел на языке Ассемблер.

Регистры общего назначения AX, BX, CX и DX

Регистры общего назначения являются основными рабочими регистрами ассемблерных программ. Их отличает то, что к ним можно адресоваться одним словом или однобайтовым кодом. Левый байт считается старшим, а правый — младшим.

Регистр AX – является основным сумматором и применяется во всех операциях ввода/вывода, в некоторых операциях со строками и в некоторых арифметических операциях. Например, команды умножения, деления и сдвига предполагают использование регистра АХ.

АХ: \ АН \ АL\

Регистр BX — базовый регистр, единственный из регистров общего назначения, используемый в индексной адресации. Кроме того, регистр BX используется при вычислениях.

BX: \ BH \ BL \

Регистр CX — является счётчиком. Он необходим для управления числом повторений циклов и для операций сдвига влево или вправо и для вычислений.

CX: \ CH \ CL \

Регистр DX — регистр данных. Используется в некоторых операциях ввода/вывода, в операциях умножения и деления больших чисел совместно с регистром AX.

DX: \ DH \ DL \

Любой из регистров общего назначения может быть использован для суммирования или вычитания 8- , 16- или 32- разрядных величин.

Команда MOV. Пересылка (байта или слова). Признаки не меняются. Рассмотрим примеры использования данной команды с применением имён, имён в квадратных скобках и чисел. В примерах предположено, что WORDAS определяет слово в памяти.

MOV AX , BX ; переслать содержимое ВX в регистр AX.

MOV AX , 28 ; переслать значение 28 в регистр AX.

MOV AX , WORDAS ; переслать WORDAS в регистр AX.

MOV AX , [ BX ] ; переслать содержимое памяти по адресу в регистре

; Bx в регистр ах.

MOV AX , [ 28 ] ; переслать содержимое по смещению 28.

Команда ADD (сложение) и SUB (вычитание) выполняют сложение и вычитание байтов или слов, содержащих двоичные данные. Вычитание осуществляется в компьютере по методу сложения с двоичным дополнением : для второго операнда устанавливаются обратные значения битов и прибавляется 1, а затем происходит сложение с первым операндом. Во всём, кроме первого шага операции сложения и вычитания идентичны. Оба операнда могут быть байтами или словами, и оба операнда могут быть двоичными числами со знаком или без знака.

Возможные ситуации сложения / вычитания: регистр – регистр; память – регистр; регистр – память; регистр – непосредственное значение; память – непосредственное значение. Например,

ADD BH , 10H ; непосредственное значение и регистр

ADD AX , BX ; регистр и регистр

ADD WORDAS , CX ; память и регистр

ADD AX , [DX] ; регистр и память

SUB WORDAS , BX ; регистр из памяти

SUB BX , 100H ; непосредственное значение из регистра

SUB WORDAS , 16H ; непосредственное значение из памяти

Один байт содержит знаковый бит и семь битов данных, т. е. результат арифметической операции может легко превзойти ёмкость регистра, и возникает переполнение. Полное слово имеет также ограничение, что ограничивает возможности компьютера для выполнения арифметических операций. Поэтому используют специальные сопроцессоры, которые быстро и эффективно выполняют эти операции, представляя числа в специальных кодах. Иногда вместо команды ADD используется команда ADC — сложения с переносом, которая складывает два значения и если флаг уже установлен, к сумме прибавляется 1. Для аналогичных целей (вычитание с заёмом) вместо команды SUB используется команда SBB.

Числовое содержимое поля может интерпретироваться по разному. Многие числовые поля являются беззнаковыми, например номер абонента, адрес памяти. Некоторые числовые поля предполагаются всегда положительными, например норма выплаты, день недели, число PI. Другие числовые поля являются знаковыми, так как их содержимое может быть положительным и отрицательным. Команды ADD и SUB не делают разницы между знаковыми и беззнаковыми данными, они просто складывают и вычитают биты.

Например, для беззнакового числа биты представляют число 249, а для знакового –7:

Assembly 8086-реализация любого умножения и деления без инструкций MUL и DIV

Как и все остальное в assembly, существует множество способов умножения и деления.

  1. Сделайте деление путем умножения на обратную величину.
  2. Используйте сдвиги и сложения/подстановки вместо умножения.
  3. Используйте параметры вычисления адреса lea (только умножение).

Разрушение мифов

потому что они требуют много циклов CPU

MUL и IMUL невероятно быстры на современных CPU, см.: http:/ / www.agner.org / optimize/instruction_tables. pdf
DIV и IDIV всегда были чрезвычайно медленными.

Пример для Intel Skylake (стр. 217):

MUL, IMUL r64: задержка 3 цикла, обратная пропускная способность 1 цикл.

Обратите внимание, что это максимальная задержка для умножения двух 64 ! битовые значения.
CPU может выполнять одно из этих умножений каждый цикл CPU, если все, что он делает, — это умножение.
Если учесть, что приведенный выше пример с использованием сдвигов и сложений для умножения на 7 имеет задержку в 4 цикла (3 с использованием lea). Нет никакого реального способа победить простое умножение на современном CPU.

Умножение на обратное

Согласно инструкции ASM lib Agner Fog Страница 12 :

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

float a, b, d;  
a /= d; b /= d;   

может быть изменен на:

float a, b, d, r;   
r = 1.0f / d;   
a *= r; b *= r;   

Если мы хотим сделать что-то подобное с целыми числами, то мы должны масштабировать обратный делитель на 2n, а затем сдвинуть n мест вправо после умножения.

Умножение на обратное хорошо работает, когда вам нужно разделить на константу или если вы делите на одну и ту же переменную много раз подряд.
Вы можете найти действительно классный код assembly, демонстрирующий эту концепцию, в библиотеке assembly Агнера Фога .

Сдвиги и добавления / замены
Сдвиг вправо — это деление на два shr — (R ед.).
Сдвиг влево-это умножение на два shl — (L arger).
Вы можете добавлять и вычитать, чтобы исправить Не-степени двух по пути.

//Multiply by 7
mov ecx,eax
shl eax,3    //*8
sub eax,ecx  //*7

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

Использование Lea
Lea-это инструкция для вычисления смещений адресов.
Он может вычислять кратные 2,3,4,5,8 и 9 в одной инструкции.
Вот так:

                      //Latency on AMD CPUs (K10 and later, including Jaguar and Zen)
                      //On Intel all take 1 cycle.
lea eax,[eax+eax]     //*2     1 cycle      
lea eax,[eax*2+eax]   //*3     2 cycles
lea eax,[eax*4]       //*4     2 cycles   more efficient: shl eax,2 (1 cycle)
lea eax,[eax*4+eax]   //*5     2 cycles 
lea eax,[eax*8]       //*8     2 cycles   more efficient: shl eax,3 (1 cycle)
lea eax,[eax*8+eax]   //*9     2 cycles

Обратите внимание, однако, что lea с множителем (масштабным коэффициентом) считается инструкцией ‘complex’ на AMD CPUs от K10 до Zen и имеет задержку 2 CPU циклов. На более ранних AMD CPUs (k8) lea всегда имеет 2-тактную задержку даже при простом режиме адресации [reg+reg] или [reg+disp8] .

ДРАМ
Таблицы инструкций Agner Fog неверны для AMD Zen: 3-компонентный или масштабируемый индекс LEA по-прежнему составляет 2 цикла на Zen (только 2 на тактовую пропускную способность вместо 4) в соответствии с InstLatx64 (http://instlatx64.atw.hu/ ). кроме того, как и ранее CPUs, в режиме 64-bit lea r32, [r64 + whatever] имеет задержку в 2 цикла. Таким образом, на самом деле быстрее использовать lea rdx, [rax+rax] вместо lea edx, [rax+rax] на AMD CPUs, в отличие от Intel, где усечение результата до 32 бит является бесплатным.

*4 и *8 можно сделать быстрее, используя shl , потому что простой сдвиг занимает всего один цикл.

С другой стороны, lea не изменяет флаги и позволяет свободно перемещаться в другой регистр назначения. Поскольку lea может сдвигаться влево только на 0, 1, 2 или 3 бита (то есть умножаться на 1, 2, 4 или 8), это единственные разрывы, которые вы получаете.

Intel
На Intel CPUs (семейство Sandybridge) любой 2-компонентный LEA (только один + ) имеет задержку в один цикл. Таким образом, lea edx, [rax + rax*4] имеет задержку в один цикл, но lea edx, [rax + rax + 12] имеет задержку в 3 цикла (и худшую пропускную способность). Пример этого компромисса подробно обсуждается в коде C++ для проверки гипотезы Коллатца быстрее, чем написанный от руки assembly-почему? .

Assembler: 45. Арифметические команды сопроцессора

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

Арифметические команды имеют следующий формат Fppp (для вещественных операндов) или FIppp (для целочисленных операндов), где ррр может принимать значения:

  • ADD – сложение
  • SUB – вычитание
  • SUBR – обратное вычитание
  • MUL – умножение
  • DIV – деление
  • DIVR – обратное деление

Возможны следующие комбинации операндов:
Fppp (FIppp) — выполняет операцию над ST(0) — приемник и ST(1) — источник. Результат заносится в регистр стека сопроцессора ST(0).

Fppp (FIppp) — выполняет операцию над значением ST(0) и источником. Результат заносится в регистр стека сопроцессора ST(0).
Fppp (FIppp) , — выполняет операцию над значением в регистре стека сопроцессора ST(i) и значением в вершине стека ST(0). Результат заносится в регистр ST(i).

FpppP (FIpppР) , — операция выполняется аналогично предыдущей команде, только последним действием команды является выталкивание значения из вершины стека сопроцессора ST(0). Результат сложения остается в регистре ST(i-1).

В чем заключается «обратность» вычитания и деления?
Проанализируйте описание этих двух команд и Вам все станет понятно:
FSUB st(i), st — команда вычитает значение в вершине стека SТ(0) из значения в регистре стека сопроцессора ST(i). Результат вычитания запоминается в регистре стека сопроцессора ST(i).
FSUBR st(i),st — команда вычитает значение в вершине стека ST(0) из значения в регистре стека сопроцессора ST(i). Результат вычитания запоминается в вершине стека сопроцессора — регистре ST(0).

Пример: сложение трех чисел:
.data
Sarray dd 1.5, 3.4, 6.6  ;объявим массив вещественных чисел, котрые мы хотим сложить
Sum dd ?  ;зарезервируем место для результата
.code 
Finit  ;инициализируем сопроцессор
Fld Sarray   ;загрузим первый элемент Sarray
Fadd Sarray+4  ;прибавим к вершине второй элемнт массива
Fadd Sarray+8  ;и третий туда же
Fstp Sum  ;выгрузим значение из вершины стека сопроцессора в переменную Sum  

Дополнительные арифметические команды

  • FSQRT ; извлечение квадратного корня
  • FSCALE ; масштабирование ST(0) = ST(0)*2ST(1), ST(1) – целое число
  • FPREM ; вычисляет частичный остаток ST(0) = ST(0) — q*ST(1), где q – целая часть результата ST(0)/ST(1)
  • FPREM1 ; вычисляет частичный остаток ST(0) = ST(0) — q*ST(1), где q – ближайшее к ST(0)/ST(1) целое число
  • FPNDINT ; округление до целого
  • FXTRACT ; расцепляет число на — порядок, который заменяет число в ST(1), мантиссу, которая помещается в ST(0)
  • FABS ; получение модуля числа
  • FCHS ; изменение знака числа

Assembler IBM PC П.1.2. Демонстрационные файлы

В данном приложении приводятся файлы, используемые при выполнении заданий к лабораторной работе № 1. Файл hello.exe может быть запущен на исполнение для отработки простого диалога с программой, так и загружен в отладчик TD, для изучения возможностей последнего при отладке ассемблерных программ. Файлы из группы – Mov.asm, Arithmet.asm Logical.asm, LoopCall.asm – демонстрируют применение соответствующей группы команд с использованием различных видов адресации. Поэтому после трансляции их загружают только в TD с целью просмотра форматов машинных команд процессора при изучении принципов их кодирования.

                        %TITLE «Демонстрационный файл Hello.asm«

IDEAL

MODEL small

STACK 256

DATASEG

Promt                        DB              ‘Это время после полудня? (Да/Нет – y/n)$’

GoodMorning            DB              13,10,’Доброе утро!’,13,10,’$’

GoodAfternoon         DB              13,10,’Здравствуйте!’,13,10,’$’

                                 CODESEG

Start:                mov ax,@data                         ;Установка в ds адреса сегмента

                        mov ds,ax                                ;данных

                        mov dx,OFFSET Promt           ;Сообщение-запрос

                        mov ah,9                                 ;Функция DOS вывода сообщения

                        int 21h                                     ;на экран

                        mov ah,1                                 ;Функция DOS ввода символа с

                        int 21h                                     ;клавиатуры

                        cmp al,’y’                                 ;y?

                        jz IsAfternoon                          ;да, время после полудня

                        cmp al,’n’                                 ;n?

                        jz IsMorning                            ;нет, до полудня

IsAfternoon:     mov dx,OFFSET GoodAfternoon               ;Указание на «Здравствуйте»

                        jmp SHORT Disp

IsMorning:       mov dx,OFFSET GoodMorning                 ;Указание на «Доброе утро»

Disp:                mov ah,9                                 ;Функция DOS вывода сообщения на

                        int 21h                                     ;экран

Exit:                 mov ax,4C00h                         ;Функция DOS- выход из программы

                        int 21h                                     ;Вызов DOS. Останов программы.

                        END Start                               ;Конец программы/точка входа

%TITLE «Команды MOV и режимы адресации. Файл Mov.asm«

                                      IDEAL

                                      MODEL small

                                      STACK 256

value                =                 528

                                      DATASEG

b_x                                DB              1,2,4

w_x                               DW             8,16,32,64

Label                             b_var          byte

w_var                            DW             1234h       ;Число в памяти:    34h(мл. байт):12h(ст. байт)

                                      CODESEG

Start:                mov ax,@data                         ;Установка в ds адреса

                        mov ds,ax                                ;сегмента данных.

;Непосредственная адресация.

                        mov al,255                              ;255=0FFh-беззнаковое число

                        mov ah,-1                      ;[4]    ;-1=0FFh-отрицательное число

                        mov ax,value/5+20         ;[5]    ;Загрузка в ах константного выражения

                        mov bx,offset w_x;         ;[6]    ;Адрес переменной w_x в bx (bx=0003h)

;Регистровая и прямая адресации. Символьная переменная, заключённая в квадратные

;скобки (например [b_x]), – выполняет роль адреса этой переменной в памяти

                        mov dl,al                        ;[7]

                        mov al,[b_x]         ;В al занести содержимое переменной b_x, т. е. al=b_x[0]=01h.

                        mov dx,[w_x]                          ;dx=w_x[0]=0008h.

                        mov si,[w_var]                         ;si=1234h

                        mov al,[b_var]                         ;al=[b_var]=34h

                        mov ah,[b_var+1]                    ;ah=[b_var+1]=12h

;Косвенная регистровая.

                        mov cx,[bx]                    ;[13]  ;cx=w_x[0]=0008h, т.к. bx=offset w_x

                        mov [word bx],-2           ;[14]  ;w_x[0]= -2=0FFFEh.

;Базовая адресация.

                        mov ax,[bx+2]               ;[15]  ;ax=w_x[1]=16=0010h.

                        mov [word bx+2],24      ;[16]  ;w_x[1]=24=0018h.

;Индексная адресация.

                        mov si,1

                        mov al,[si+b_x]              ;[18]             ;al=[b_x+1]=02h.

;Базово индексная адресация.

                        inc si                                        ;si=2

                        mov bx,1                                 ;bx=2

                        mov ax,[bx+si+w_x]                ;[21]    ;ax=[4+w_x]=32=0020h.

                        mov [word bx+si+w_x],128    ;[22]    ;w_x[2]=128=0080h.

;Применение команды lea

                        lea bx,[w_x+si]              ;[23]  ;bx=offset w_x+si=offset w_x[1]=005h.

Exit:                 mov ax,4C00h                         ;Функция DOS- выход из программы.

                        int 21h                                     ;Вызов DOS. Останов

                        END Start                               ;Конец программы/точка входа.

%TITLE «Команды сложения, умножения и деления. Файл Arithmet.asm«

IDEAL

MODEL small

STACK 256

                                    DATASEG

op_1                DD                  11112222h

op_2                DD                  3333DDDEh

b_dst               DB                  32                        ;20h

b_src               DB                  64                        ;40h

w_src               DW                 512                      ;200h

                                      CODESEG

Start:           mov ax,@data                    ;Установка в ds адреса

                   mov ds,ax                           ;сегмента данных.

;Сложение операндов из двойных слов.

                   mov di,offset op_1

                   mov si,offset op_2

                   mov ax,[di]                ;Low(op_1)®ax.

                   add ax,[si]                 ;[6]      ;Low(op_1)+Low(op_2)=Low(sum).

                   mov [di],ax                            ;Сохранение Low(sum).

                   mov ax,[di+2]                       ;High(op_1)®ax.

                   adc ax,[si+2]             ;[9]      ;High(op_1)+High(op_2)+cf=High(sum).

                   mov [di+2],ax            ;Сохранение High(sum).

;Умножение и деление.

                 mov al,[b_dst]                       ;al=32=20h.

                 Push al

                   mul [b_src]                ;[13]    ;ax ¬al*[b_src]- беззнаковое умножение: «8*8=16»

                   neg [b_src]                ;[14]    ;[b_src]¬0-[b_src]

                   pop al

                   imul [b_src]               ;[16]    ;ax ¬al*[b_src]- знаковое умножение: «8*8=16»

                   idiv [b_src]                ;[17]    ;{al= Quot (ax/[b_src]), ah=Rem (ax/[b_src])} – знако-

                                                                ;вое деление: «16:8=8»

                   cbw                                      ;al®ax (со знаком). В данном случае ax>0

                   mul [w_src]             ;[19 ]    ;dx:ax¬ax*[w_src]- беззнаковое умножение: «16*16=32»

                   idiv [w_src]               ;[20 ]   ;{ax¬Quot (dx.ax/w_src), dx¬Rem (dx.ax/w_src)} –

;знаковое деление в формате: «32:16=16». Так как операнды положительные, то такой же

;результат можно было бы получить и с помощью команды div

Exit:          mov ax,4C00h                      ;Функция DOS -выход из программы.

                 int 21h                                   ;Вызов DOS. Останов.

                 END Start                             ;Конец программы/точка входа.

%TITLE «Логические команды и команды сдвига. Файл Logical.asm«

IDEAL

MODEL small

STACK 256

DATASEG

source                  DW                      0ABh

w_mask                DW                      0F0h

oper                     DB                       0AAh          ;176

                                      CODESEG

Start:                mov ax,@data                    ;Установка в ds адреса сегмента

                        mov ds,ax                           ;данных.

                        mov ax,[source]                  ;Занести в ax,bx,cx, [source]=0ABh

                        mov bx,ax

                        mov cx,ax

;Стандартное применение логических команд

                        and ax,[w_mask]       ;[6]    ;Стирание соответствующих битов

                        or bx,[w_mask]         ;[7]    ;Установка соответствующих битов в «1»

                        xor cx,[w_mask]                 ;Инвертирование соответствующих битов

                        xor bx,bx                            ;bx=0. Гашение регистра

;Циклические сдвиги.

                        rol [oper],1                ;[10]  ;[oper]=55, cf=1.

                        ror [oper],1                         ;[oper]=AA, cf=1.

                        rcl [oper],1                ;[12]  ;[oper]=55, cf=1.

                        rcr [oper],1                         ;[oper]=AA, cf=1.

;Нестандартное применение – быстрое деление положительного числа сдвигами вправо.

                        mov al,0Eh                          ;al=0Eh=14

                        sar al,1                      ;[15]  ;al=07 ,cf=0,

                        sar al,1                                ;al=03 ,cf=1,

                        sar al,1                                ;al=01 ,cf=1,

                        sar al,1                                ;al=00 ,cf=1.

;Быстрое умножение сдвигами влево положительного числа: A=10*x=(4+1)*2*x; x=al.

                        mov al,2                              ;al=2

                        mov bl,al

                        sal al,1                       ;[21]  ;*2,

                        shl al,1                                ;*4,

                        add al,bl                              ;*(4+1),

                        shl al,1                                ;*10, al=10*x=20=14h.

Exit:                 mov ax,4C00h                    ;Функция DOS -выход из программы.

                        int 21h                                ;Вызов DOS. Останов.

                        END Start                          ;Конец программы/точка входа.

%TITLE «Цикл с подпрограммой. Файл LoopCall.asm«

;В программе демонстрируется организация цикла на основе команды Loop в процессе деле-

;ния каждого элемента массива Array на постоянное число с использованием соответствую-

;щей процедуры Divide

IDEAL

MODEL small

STACK 256

DATASEG

Array                    DB              20,25,30,35,40

LengArray            =                 $-Array                ;длина строки

                                    CODESEG

Start:                mov ax,@data                              ;Установка в ds адреса сегмента

                        mov ds,ax                                    ;данных.

                        mov si,offset Array

                        mov cx,LengArray

L1:                   mov al,[si]                                    ;al¬текущий элемент строки.

                        call Divide             ;[6]                  ;Выполнение процедуры деления на 5.

                        mov [si],al

                        inc si                    

                        loop L1                ;[9]                  ;Повторить сх раз

Exit:                 mov ax,4C00h                             ;Функция DOS -выход из программы.

                        int 21h                                          ;Вызов DOS. Останов.

;Подпрограмма деления Divide на 5.      Вход: al-значение, предназначенное для деления.

                                                                  ;Выход: al-результат деления.

PROC             Divide near                                   ;Оператор near можно не указывать, т.к. модель

                        ;памяти Small предполагает все переходы близкими

                        push bx                                       

                        xor ah,ah                                      ;Подготовка ah:al как 16-битовое

                        mov bl,5                                       ;делимое, а bl- 8-битовый делитель.

                        div bl                                            ;al<quot(ax/bl), ah<-rem(ax/bl)

                        pop bx

                        ret                        ;[18]                ;Возврат из процедуры

ENDP              Divide

                        END Start                                    ;Конец программы/точка входа.

Использование процессора

— Сборка 8086 — Реализация любого умножения и деления без инструкций MUL и DIV

Как и все остальное в ассемблере, есть много способов умножения и деления.

  1. Сделайте деление, умножив на обратное значение.
  2. Используйте сдвиги и добавления / подстановки вместо умножения.
  3. Используйте параметры вычисления адреса lea (только умножение).

Разрушение мифов

, потому что они требуют большого количества циклов ЦП

MUL и IMUL невероятно быстры на современных процессорах, см. Http: // www.agner.org/optimize/instruction_tables.pdf
DIV и IDIV работают и всегда были чрезвычайно медленными.

Пример для Intel Skylake (стр. 217):

MUL, IMUL r64: Задержка 3 цикла, обратная пропускная способность 1 цикл.

Обратите внимание, что это максимальная задержка при умножении на два 64! битовые значения.
ЦП может выполнять одно из этих умножений в каждом цикле ЦП, если все, что он делает, — это умножения.
Если учесть, что в приведенном выше примере с использованием сдвигов и сложения для умножения на 7 задержка составляет 4 цикла (3 с использованием lea).На современном процессоре нет реального способа превзойти простое умножение.

Умножение на обратную

Согласно инструкции Agner Fog asm lib на странице 12:

Деление работает медленно на большинстве микропроцессоров. С плавающей запятой вычисления, мы можем сделать несколько делений с одним и тем же делителем быстрее, умножив на обратную, например:

  поплавок a, b, d;
а / = d; b / = d;
  

можно изменить на:

  поплавок a, b, d, r;
г = 1.0f / d;
а * = г; б * = г;
  

Если мы хотим сделать что-то подобное с целыми числами, мы должны масштабировать обратный делитель на 2n, а затем сдвинуть n разрядов на сразу после умножения.

Умножение на обратное хорошо работает, когда вам нужно разделить на константу или если вы делите на одну и ту же переменную много раз подряд.
Вы можете найти действительно крутой ассемблерный код, демонстрирующий концепцию, в ассемблерной библиотеке Агнера Фога.

Сдвиг и добавление / подпрограмма
Сдвиг вправо — это деление на два shr — ( R educe).
Сдвиг влево — это умножение на два shl — ( L arger).
Вы можете складывать и вычитать, чтобы корректировать не степени двойки по пути.

  // Умножить на 7
mov ecx, eax
shl eax, 3 // * 8
sub eax, ecx // * 7
  

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

Использование Lea
Lea — это инструкция для вычисления смещения адресов.
Он может вычислять кратные 2,3,4,5,8 и 9 за одну инструкцию.
Как так:

  // Задержка на процессорах AMD (K10 и новее, включая Jaguar и Zen)
                      // На Intel все занимают 1 цикл.
lea eax, [eax + eax] // * 2 1 цикл
lea eax, [eax * 2 + eax] // * 3 2 цикла
lea eax, [eax * 4] // * на 4 цикла эффективнее: shl eax, 2 (1 цикл)
lea eax, [eax * 4 + eax] // * 5 2 цикла
lea eax, [eax * 8] // * 8 на 2 цикла эффективнее: shl eax, 3 (1 цикл)
lea eax, [eax * 8 + eax] // * 9 2 цикла
  

Обратите внимание, что lea с множителем (масштабным коэффициентом) считается «сложной» инструкцией на процессорах AMD от K10 до Zen и имеет задержку в 2 цикла процессора.На более ранних процессорах AMD (k8) lea всегда имеет двухцикловую задержку даже в простом режиме адресации [reg + reg] или [reg + disp8] .

AMD
Таблицы инструкций Агнера Фога неверны для AMD Zen: 3-компонентный или масштабируемый индекс LEA по-прежнему составляет 2 цикла в Zen (с пропускной способностью только 2 на такт вместо 4) в соответствии с InstLatx64 (http: // instlatx64. atw.hu/). Также, как и более ранние процессоры, в 64-битном режиме lea r32, [r64 + любой] имеет задержку в 2 цикла.Так что на самом деле быстрее использовать lea rdx, [rax + rax] вместо lea edx, [rax + rax] на процессорах AMD, в отличие от Intel, где усечение результата до 32 бит является бесплатным.

* 4 и * 8 можно сделать быстрее, используя shl , потому что простой сдвиг занимает всего один цикл.

С положительной стороны, lea не изменяет флаги и позволяет свободный переход к другому регистру назначения. Поскольку lea может сдвигаться влево только на 0, 1, 2 или 3 бита (то есть умножение на 1, 2, 4 или 8), это единственные разрывы, которые вы получаете.

Intel
На процессорах Intel (семейство Sandybridge) любой двухкомпонентный LEA (только один + ) имеет задержку в один цикл. Итак, lea edx, [rax + rax * 4] имеет задержку в один цикл, но lea edx, [rax + rax + 12] имеет задержку в 3 цикла (и худшую пропускную способность). Пример этого компромисса подробно обсуждается в коде C ++ для проверки гипотезы Коллатца быстрее, чем рукописная сборка — почему ?.

Умножения, деления и сдвиги

Поскольку Z80 не имеет встроенных инструкций умножения, когда программист хочет произвести умножение, он должен выполнить это вручную.Программист может взять процедуру умножения из книги или журнала, которая не обязательно может быть оптимизирована для конкретного случая (или вообще оптимизирована), или он может даже использовать процедуру, которая работает, добавляя одно и то же значение n раз, то есть в значительной степени худшее решение проблемы.

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

8-битное умножение со сдвигом

Когда вы сдвигаете регистр на 1 бит влево, вы умножаете значение регистра на 2. Этот сдвиг можно выполнить с помощью инструкции SLA r. Последовательно выполняя несколько сдвигов, вы можете очень легко умножить на любую степень 2. Например:

 л.д. б, 3; Умножить 3 на 4
    sla b; x4
    sla b; результат: b = 12
 

Если вы используете регистр A, вы можете быстрее умножать с помощью инструкции ADD A, A, которая составляет 5 T-состояний на инструкцию вместо 8.Таким образом, ADD A, A в точности совпадает с SLA A или умножением на два. Кстати, вместо использования ADD A, A вы также можете использовать RLCA, который фактически ведет себя так же.

 л.д. а, 15; Умножить 15 на 8
    добавить a, a; x8
    добавить, а
    добавить a, a; результат: a = 120
 

При программировании умножения вы всегда должны быть уверены, что результат никогда не превысит 255, другими словами, перенос может не произойти. В этом случае RLCA фактически действует иначе, чем SLA A или ADD A, A (в некоторых случаях более полезно, в некоторых случаях менее полезно).Но, как правило, это не вызывает беспокойства, потому что, когда регистр A переполняется, результат обычно не имеет особого смысла.

Если вы хотите умножить на другое значение, кроме степени двойки, вы почти всегда можете достичь желаемого результата, сохраняя промежуточные значения во время сдвига и добавляя или вычитая их впоследствии. Несколько примеров:

 л.д. а, 5; Умножьте 5 на 20 (= A x 16 + A x 4)
    добавить a, a; x16
    добавить, а
    ld b, a; Сохраните значение A x 4 в B
    добавить, а
    добавить, а
    добавить a, b; Добавьте A x 4 к A x 16, результат: a = 100
 

Если вы хотите умножить на 22, вы также должны сохранить значение после первого прибавления в регистре и затем прибавить его к общей сумме.

Иногда вы также можете использовать вычитания для более быстрого достижения целей. Например, умножение A на 15. Это можно сделать, используя метод, описанный выше, однако в этом случае вам понадобятся 4 временных регистра и четыре дополнительных добавления впоследствии. Это можно было бы лучше сделать следующим образом, для чего требуется еще 1 умножение, но затем используется только 1 временный регистр и 1 вычитание:

 л.д. а, 3; Умножьте 3 на 15 (= A x 16 - A x 1)
    ld b, a; Сохранить значение A x 1 в B
    добавить a, a; x16
    добавить, а
    добавить, а
    добавить, а
    sub b; результат: a = 45
 

8-битное деление со сдвигом

Так вот, деление очень похоже на умножение.Если процедура умножения сложна, процедура деления еще более сложна. Однако, используя сдвиги, слишком легко разделить в Assembly. Это делается простым смещением в другую сторону, вправо. Для этого следует использовать инструкцию SRL r. Пример:

 л.д. б, 3; Разделить 18 на 4
    srl b; x4
    srl b; результат: b = 4 (остальные 2 потеряны)
 

Нет реальной быстрой альтернативы сдвигу вправо при использовании регистра A. В качестве альтернативы вы можете использовать для этого RRCA.Однако при использовании RRCA необходимо убедиться, что отдыха не будет, иначе результат не будет правильным. Это может быть достигнуто операцией AND к исходному значению со значением, очищающим младшие биты (которые в противном случае были бы вывернуты), или путем использования значений, которые всегда кратны разделителю.

 л.д. а, 153; Разделить 153 на 8
    и a,% 11111000; Очистить биты 0–2 (равно 256–8)
    rrca; / 8
    rrca
    rrca; результат: a = 19
 

Деление со значениями, отличными от степени двойки, сложнее и часто невозможно.Если вы хотите убедиться в этом сами, попробуйте построить процедуру, которая делит 100 на 20. Чтобы узнать, можно ли разделить значение, вы должны посмотреть на количество завершающих нулей в двоичном представлении этого значения. В этом случае максимальное количество RRCA, которое вы можете использовать, равно этому количеству. Если вы посмотрите на вышеупомянутое значение 100 в двоичном формате (% 01100100), вы увидите, что там два конечных нуля. Поэтому в подпрограмме деления можно использовать только 2 RRCA, тогда как для деления на 20 требуется 4 из них.

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

16-битные сдвиги

Есть также способы сдвига 16-битных регистров. Это делается с помощью 8-битного сдвига в сочетании с 8-битным поворотом и битом переноса. Чтобы сдвинуть регистр DE на один бит влево, вы должны использовать:

 слэ
    rl d
 

Чтобы сдвинуть его на один бит вправо (теперь с BC в качестве примера), используйте:

 srl b
    rr c
 

К сожалению, обычно эти 16-битные сдвиги довольно медленны по сравнению с 8-битными сдвигами (которые чаще всего происходят в быстром регистре A, что делает их почти в 4 раза быстрее).Однако, как и в случае с 8-битными сдвигами, есть также возможность выполнять более быстрые 16-битные сдвиги влево с помощью инструкции ADD.

 доп. Гл., Гл .; сдвинуть HL на 1 бит влево ... hl = hl x 2
 

Итак, лучший способ умножить 16-битные значения — использовать регистр HL в сочетании с инструкциями ADD HL, HL.

Быстрые универсальные процедуры умножения

Ну вот и все, что можно сказать о сдвигах и умножениях / делениях. Есть еще несколько второстепенных приемов, но на самом деле все, что я могу сказать об этом, — это просто проявить немного творчества.

Наконец, я расскажу вам, насколько мне известно, самые быстрые из возможных универсальных процедур умножения и деления. Эти процедуры следует использовать, когда:

  • оба параметра умножения / деления неизвестны,
  • , если вы хотите разделить на другое значение, отличное от степени 2, или вам нужно остальное значение,
  • , если известное значение является таким, что его невозможно вычислить с помощью описанных выше методов.

Вы также можете использовать их, если хотите, чтобы ваш код выглядел действительно аккуратно, и не особо заботитесь о скорости.Что касается их внутреннего устройства, все, что я скажу о нем, — это то, что они работают так же, как вы научились решать умножения и деления в начальной школе. Но на самом деле это не имеет значения. Просто скопируйте их в свою программу как (развернутые) подпрограммы или, если вы хотите удалить любое понятие структурированного программирования, как встроенный код или макрос.

Да, и если у вас действительно очень низкие переменные коэффициенты умножения, вы можете использовать ужасный метод умножения на добавление x-раз-цикла.Наверное, быстрее.

Умножение с вращением влево

Это подпрограммы умножения с вращением влево. Их скорость в основном довольно постоянна, хотя в зависимости от количества единиц в первичном множителе может быть небольшая разница в скорости (1 обычно занимает на 7 состояний больше времени, чем 0).

;
; Умножение 8-битных значений
; В: Умножить H на E
; Out: HL = результат
;
Mult8:
    ld d, 0
    ld l, d
    ЖК б, 8
Mult8_Loop:
    добавить hl, hl
    младший NC, Mult8_NoAdd
    добавить hl, de
Mult8_NoAdd:
    djnz Mult8_Loop
    Ret
 
;
; Умножьте 8-битное значение на 16-битное значение
; В: Умножить A на DE
; Out: HL = результат
;
Mult12:
    ld l, 0
    ЖК б, 8
Mult12_Loop:
    добавить hl, hl
    добавить, а
    младший NC, Mult12_NoAdd
    добавить hl, de
Mult12_NoAdd:
    djnz Mult12_Loop
    Ret
 
;
; Умножение 16-битных значений (с 16-битным результатом)
; В: Умножение BC на DE
; Out: HL = результат
;
Mult16:
    ld a, b
    ЖК б, 16
Mult16_Loop:
    добавить hl, hl
    sla c
    rla
    младший NC, Mult16_NoAdd
    добавить hl, de
Mult16_NoAdd:
    djnz Mult16_Loop
    Ret
 
;
; Умножение 16-битных значений (с 32-битным результатом)
; В: Умножение BC на DE
; Выход: BCHL = результат
;
Mult32:
    ld a, c
    ld c, b
    ld hl, 0
    ЖК б, 16
Mult32_Loop:
    добавить hl, hl
    rla
    rl c
    младший NC, Mult32_NoAdd
    добавить hl, de
    adc a, 0
    jp nc, Mult32_NoAdd
    inc c
Mult32_NoAdd:
    djnz Mult32_Loop
    ld b, c
    ld c, a
    Ret
 

Умножение с правым вращением

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

Однако у делений, вращающихся вправо, есть одно довольно приятное преимущество, которое состоит в том, что он должен выполнять цикл только до тех пор, пока не закончатся биты, после чего он может быть завершен без каких-либо дополнительных операций. Таким образом, если значение на самом деле использует только 4 бита (например, число 11), процедура должна выполняться только 4 цикла и может быть завершена впоследствии с помощью быстрой условной проверки, поэтому для этого потенциально большого ускорения практически не требуется дополнительных усилий.

Глядя на приведенную ниже таблицу с расчетами скорости подпрограммы Mult12R и учитывая тот факт, что оптимальный левовращающийся Mult12 (развернутый) принимает 268 T-состояний (включая состояния M1, что в основном означает 1 дополнительное состояние для каждой инструкции), вы можете увидеть, что в граница эффективности составляет 4 бита. Более того, обычный Mult12 работает быстрее. Однако 4-битный Mult12, конечно, снова будет более быстрым, поэтому, чтобы воспользоваться этим, большинство, но не все значения должны попадать в 4-битный диапазон.

Сколько времени занимает n-битное умножение (в среднем):
1-битное: 78,5 T-состояний
2-битное: 140 T-состояний
3-битное: 201,5 T-состояний
4-битное: 263 T- состояний
5-бит: 324,5 T-состояний
6-бит: 386 T-состояний
7-бит: 447,5 T-состояний
8-бит: 509 T-состояний

Важно отметить, что это может показаться быстрым, но не забывайте, что количество битов и возможные значения связаны друг с другом в логарифмической шкале. Поэтому, если вы используете случайные байтовые значения, количество значений в 4-битном диапазоне составляет только 1 из каждых 16 значений.Чтобы лучше понять, что это означает, взгляните на следующую таблицу, в которой указана средняя скорость, которую вы можете ожидать для случайного значения в определенном домене.

Средние скорости в указанном домене:

<0,255> 447,980 T-состояния
<0,127> 386,961 T-состояния
<0,6322> 90,4268 -состояние
<0,31> 266.844 T-состояния
<0,15> 209,188 T-состояния
<0,7> 155,375 T-состояния
<0,3> 109,25 T-состояния
<0,1> 78,5 T-состояния

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

Ну вот собственно рутина. Обратите внимание, что ее можно легко преобразовать в подпрограмму Mult8R, вставив инструкцию ld d, 0 сразу после ld hl, 0, а также что развертывание этой подпрограммы не даст вам дополнительной скорости..

;
; Умножьте 8-битное значение на 16-битное значение (вращение вправо)
; В: Умножить A на DE
; Поместите наименьшее значение в A для наиболее эффективного расчета
; Out: HL = результат
;
Mult12R:
    ld hl, 0
Mult12R_Loop:
    srl a
    jr nc, Mult12R_NoAdd
    добавить hl, de
Mult12R_NoAdd:
    Sla E
    rl d
    или
    jp nz, Mult12R_Loop
    Ret
 

Развернутое умножение с вращением влево

Для полноты картины здесь также пример развернутой процедуры умножения с вращением влево.Это занимает немного больше места, но работает значительно быстрее. На самом деле он все еще довольно компактный, всего 41 байт. В среднем на это требуется 268 T-состояний (включая ожидания M1). Минимум — 235 состояний, максимум — 301 тик.

Между прочим, я рассмотрел возможность использования той же техники, что и при умножении с правым вращением в этой процедуре, это довольно легко сделать, поместив переходы между ними в список add hl, hl. Однако потеря скорости, вызванная дополнительными прыжками, не перевешивает выигрыш, и в любом случае это вряд ли практично, поскольку это применимо к nr.битов, используемых слева (число 128 будет использовать 1 бит, а 64 — два).

;
; Умножение 8-битного значения на 16-битное значение (развернутое)
; В: Умножить A на DE
; Out: HL = результат
;
Mult12U:
    ld l, 0
    добавить, а
    младший NC, Mult12U_NoAdd0
    добавить hl, de
Mult12U_NoAdd0:
    добавить hl, hl
    добавить, а
    младший NC, Mult12U_NoAdd1
    добавить hl, de
Mult12U_NoAdd1:
    добавить hl, hl
    добавить, а
    младший NC, Mult12U_NoAdd2
    добавить hl, de
Mult12U_NoAdd2:
    добавить hl, hl
    добавить, а
    младший NC, Mult12U_NoAdd3
    добавить hl, de
Mult12U_NoAdd3:
    добавить hl, hl
    добавить, а
    младший NC, Mult12U_NoAdd4
    добавить hl, de
Mult12U_NoAdd4:
    добавить hl, hl
    добавить, а
    младший NC, Mult12U_NoAdd5
    добавить hl, de
Mult12U_NoAdd5:
    добавить hl, hl
    добавить, а
    младший NC, Mult12U_NoAdd6
    добавить hl, de
Mult12U_NoAdd6:
    добавить hl, hl
    добавить, а
    ret nc
    добавить hl, de
    Ret
 

Также стоит развернуть другие процедуры умножения.Например, для процедуры Mult8 она сохраняет 115 из 367 T-состояний в среднем, что на 31% быстрее, всего за 38 байт (14 → 52).

Быстрые процедуры общего деления

Прежде чем мы перейдем к реальным процедурам деления, при делении необязательно использовать (более медленные) процедуры деления. Не забывайте, умножение и деление связаны. Если вы хотите разделить на 2, вы также можете умножить на 0,5. Преобразуя это в эти подпрограммы, чтобы разделить 8-битное значение на другое 8-битное значение (как это делает Div8), вы можете вызвать подпрограмму Mult8 с параметром 1 (1 / первое 8-битное значение * 256) и параметром 2 другое 8-битное значение.Результирующее слово будет шестнадцатеричным значением с фиксированной точкой с запятой между старшим и младшим байтами.

Например, вычислите 55/11:

Вход A: # 18 (1/11 * 256 = 23,272727, округлено до 24)
Вход B: # 37 (55)
Выход: # 528 (# 5,28 или 5,15625 в десятичной системе)

Обратите внимание, что результат не совсем верное значение (которое должно было быть # 500), потому что 1/11 на самом деле не очень хорошее число, как в десятичном представлении (.0

), так и в шестнадцатеричном представлении (#.1745D1). Если бы мы округлили значение не до # .18, а до # .17, что было бы правильным округлением, результат был бы # 4F1. Это было бы более точным, чем текущий результат, однако наша цель — написать оптимальный код, и выделить правильно округленное целое число из этого результата (равного 5) было бы намного проще, если бы в результате у нас получилось # 528, потому что мы тогда можно просто взять только старший байт. Также для «правильного округления» потребуется дополнительный код. Вот почему мы округлили значение.

Если взять слишком большое базовое значение, ошибка накапливается. Если вы, например, попытаетесь разделить 2200 на 11, результатом будет 206, тогда как должно было быть 200. Для решения этой проблемы вы можете либо увеличить разрешение (используйте 16-битное значение деления (# .1746) и 24 — или 32-битный результат) или разделите на значения, которые являются «аккуратными» в шестнадцатеричной системе счисления (являющиеся степенями двойки). Однако также помните, что независимо от того, какую базу вы используете, будь то 10 или 16, у вас всегда будут подразделения, которые приводят к ошибкам.Значения, с которыми это происходит, различаются, но вам придется иметь дело с ограничениями разрешения и округлением.

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

;
; Разделить 8-битные значения
; В: Разделить E на делитель C
; Выходы: A = результат, B = отдых
;
Div8:
    xor a
    ЖК б, 8
Div8_Loop:
    rl e
    rla
    sub c
    младший NC, Div8_NoAdd
    добавить a, c
Div8_NoAdd:
    djnz Div8_Loop
    ld b, a
    ld a, e
    rla
    cpl
    Ret
 
;
; Разделить 16-битные значения (с 16-битным результатом)
; В: Разделить BC на делитель DE
; Аут: BC = результат, HL = отдых
;
Div16:
    ld hl, 0
    ld a, b
    ЖК б, 8
Div16_Loop1:
    rla
    adc hl, hl
    sbc hl, de
    младший NC, Div16_NoAdd1
    добавить hl, de
Div16_NoAdd1:
    djnz Div16_Loop1
    rla
    cpl
    ld b, a
    ld a, c
    ld c, b
    ЖК б, 8
Div16_Loop2:
    rla
    adc hl, hl
    sbc hl, de
    младший NC, Div16_NoAdd2
    добавить hl, de
Div16_NoAdd2:
    djnz Div16_Loop2
    rla
    cpl
    ld b, c
    ld c, a
    Ret
 

Спасибо Flyguille за подпрограмму Div16, взятую из его исходного кода MNBIOS.

Подпрограммы деления также можно развернуть, чтобы получить хороший прирост скорости. Если вы возьмете, например, подпрограмму Div16, для завершения требуется 1297 T-состояний. Однако в развернутом состоянии ему нужно всего 1070 T-состояний, что означает увеличение скорости на 18%. Дополнительная стоимость в байтах составляет 146 байтов (исходная процедура — 27 байтов).

Деление на 9

Рикардо Биттенкур предоставил нам быструю процедуру деления на 9. Она создана для .dsk или подпрограммы ROM. Это очень быстро, но работает только в диапазоне 0–1440.

;
; деление на девять
; введите HL = число от 0 до 1440
; выход A = HL / 9
; уничтожить HL, DE
; Z80 R800
DIV9: INC HL; 7 1
        LD D, H; 5 1
        LD E, L; 5 1
        ДОБАВИТЬ HL, HL; 12 1
        ДОБАВИТЬ HL, HL; 12 1
        ДОБАВИТЬ HL, HL; 12 1
        SBC HL, DE; 17 2
        LD E, 0; 8 2
        LD D, L; 5 1
        LD A, H; 5 1
        ДОБАВИТЬ HL, HL; 12 1
        ДОБАВИТЬ HL, HL; 12 1
        ADD HL, DE; 12 1
        АЦП A, E; 5 1
        XOR H; 5 1
        И 03FH; 8 2
        XOR H; 5 1
        RLCA; 5 1
        RLCA; 5 1
        RET; всего = 157 22
 

Процедура извлечения квадратного корня

Это более быстрая процедура извлечения квадратного корня, чем та, которая была здесь ранее, результаты тестов говорят, что она на 26% быстрее.Его написал Рикардо Биттенкур, огромное ему спасибо :).

;
; Квадратный корень из 16-битного значения
; В: HL = значение
; Out: D = результат (округлено в меньшую сторону)
;
Sqr16:
    ld de, # 0040
    ld a, l
    ld l, h
    ld h, d
    или
    ЖК б, 8
Sqr16_Loop:
    sbc hl, de
    младший NC, Sqr16_Skip
    добавить hl, de
Sqr16_Skip:
    ccf
    rl d
    добавить, а
    adc hl, hl
    добавить, а
    adc hl, hl
    djnz Sqr16_Loop
    Ret
 

Ну вот и все. Спасибо господину. Родней Закс :), за написание его книги «Программирование Z80», которая научила меня выполнять умножение на Z80 и послужила источником вдохновения для перечисленных здесь процедур.Если у вас есть предложения по повышению скорости или вы знаете другой метод, который может быть быстрее при определенных условиях, сообщите мне.

~ Grauw

О языке ассемблера — подраздел

Язык ассемблера — Подразделение

Сокращенный набор команд для всех микросхем в семействе ARM — от ARM2 до StrongARM — включает странные и замечательные инструкции, такие как MLA (Умножение на Накопление: умножение на два регистров и добавить к результату содержимое трети) и ASL (арифметический сдвиг влево: полностью идентична инструкции Logical Shift Left).Более поздние чипы содержат несколько Дополнительные указания. Однако, насколько мне известно, места в набор инструкций для чего-то, что было бы очень полезно — инструкции DIV.

Принципы деления

Деление в машинном коде точно такое же, как деление любым другим методом — это просто дело повторного вычитания. Когда мы делим двенадцать на четыре, мы действительно хотим знать сколько раз число 4 уместится в число 12, другими словами, сколько раз мы можем вычтите меньшее число из большего числа, пока большее число не достигнет нуля.В ответ в этом случае, конечно, будет три раза:
12 - 4 = 8
8 - 4 = 4
4-4 = 0
 

Когда я был совсем маленьким, мне разрешили играть со старомодными механическими счетчиками. машина. На передней панели машины было множество вертикальных циферблатов, подобных тем, что на кодовый замок, на котором вы устанавливаете числа, которые хотите вычислить, и был ручка на одной стороне, которая была намотана от вас, чтобы прибавить текущий номер к итоговое значение на дисплее или по направлению к вам, чтобы вычесть его.Чтобы осуществить разделение, нужно было установить первое число, а затем вычесть из него второе число несколько раз (считая, сколько раз вы поворачивали ручку!), как и в приведенной выше сумме. Очевидно, однако, что это могло бы быть очень медленным, если бы вы делали сумму вроде 128 ÷ 4. поскольку ответ — 32, это количество раз, которое вам придется повернуть ручку!

Было бы вполне возможно выполнить деление в коде ARM, используя эту простую технику, построив такой цикл:

 MOV R1, # 128; разделить R1
 MOV R2, # 4; от R2
 MOV R0, # 0; инициализировать счетчик
.вычесть
 SUBS R1, R1, R2; вычесть R2 из
                   ; R1 и магазин
                   ; результат обратно в
                   ; R1 установка флагов
 ADD R0, R0, # 1; добавить 1 к счетчику,
                   ; НЕ устанавливаем флаги
 BHI вычесть; перейти к началу
                   ; цикл по условию
                   ; Выше, т.е. R1 равно
                   ; все еще больше, чем
                   ; R2. Ответьте сейчас в R0
 

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

Перемещение по

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

Используя наш пример 128 ÷ 4, вы в конечном итоге сделаете следующее:

  • Сдвинуть 4 на одну позицию влево — станет 40.
  • Shift 4 на другое место влево — становится 400. Это больше 128, так что мы зашли слишком далеко. Начнем с предыдущего результата — 40. Итак:
  • 128-40 = 88 (поворот ручки один раз)
  • 88-40 = 48 (поворот дважды)
  • 48-40 = 8 (трижды повернув)
  • Мы больше не можем вычесть 40, поэтому мы обнаружили, что первая цифра ответа — 3.
  • Теперь сдвигаем наше число назад вправо, чтобы оно снова стало 4, и снова начинаем считать обороты ручки.
  • 8-4 = 4 (поворот ручки один раз)
  • 4-4 = 0 (поворот дважды)

Мы больше не можем вычесть 4 (наше исходное число), поэтому мы нашли последнюю цифру числа ответ будет 2. Другими словами, ответ будет 32, результат, который мы получили раньше, но у нас было обернуть ручку только пять раз, чтобы получить ее, а не тридцать два раза!

Вы могли заметить, что это почти тот же метод, который используется при делении с использованием ручка и бумага… «четыре в один не пойдут … четыре в двенадцать входят три раза … четыре в восемь идет дважды … ответ тридцать два «. Разница в том, что мы знаем по опыту, что 12 — это 4 × 3, а 8 — это 4 × 2. Машины, даже электронные, такие как компьютеры, не имеют этого преимущества — они не содержат справочного набора таблиц умножения, поэтому они должны делать это по-своему.

Двоичное деление

Компьютеры используют двоичные числа. Вы не поверите, но это на самом деле делает жизнь намного проще, когда приходит к написанию процедуры машинного кода для выполнения деления одного регистра на другой! Поскольку значение каждой двоичной цифры ответа может быть только 0 или 1, мы можем избежать упомянутая выше «проблема таблицы умножения».

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

Деление на два

Здесь я воспользуюсь новым примером. Причина в том, что в двоичной системе деление на четыре или любая другая степень двойки чрезвычайно проста; ни один здравомыслящий программист не напишет сложную процедуру деления, когда все, что ему нужно сделать, это использовать одну инструкцию для сдвинуть соответствующий регистр вправо, не больше, чем он достанет ручку и бумагу, чтобы вычислить ответ на сумму «460 ÷ 10» вместо того, чтобы мысленно сбивать ноль, чтобы получить правильный ответ.Разумная программа для деления 128 на 4 выглядела бы так:
 MOV R1, # 128
 MOV R0, R1, LSR # 2; смена R1 2 разряда
                  ;Направо &
                  ; хранить в R0
                  ; ответьте сейчас в R0
 

Поскольку 4 равно 2 × 2, все, что нам нужно сделать, чтобы разделить на 4 в двоичном формате, — это сдвинуть регистр на два. местами вправо, так же как все, что нам нужно сделать, чтобы разделить на 100 (10 × 10) в десятичной дроби, это сдвинуть на два места вправо — например, от 600 пенсов получаем 6 фунтов.

Разделение на другие числа

Однако этот подход работает только тогда, когда мы хотим разделить на фиксированное значение, которое, как нам известно, является кратно двум.Для более общего использования нам нужно использовать подход, основанный на вычитании, описанный в общих чертах. выше.

Чтобы разделить 50 (% 110010) на 10 (% 1010) в двоичном формате:

  • Мы сдвигаем 10 насколько возможно (на два места) влево, пока оно не станет% 101000, и вычтем это из% 110010, чтобы получить первая цифра.
  • % 110010-% 101000 =% 1010 (первая цифра 1)
  • Теперь мы сдвигаем% 101000 назад на одну позицию вправо и пытаемся вычесть% 10100 из того, что осталось от 25, с которых мы начали.
  • % 1010 -% 10100 (Следующая цифра 0 — не пойдет!)
  • Сдвинуть вправо, получить% 1010 и повторить попытку:
  • % 1010 -% 1010 =% 0 (Успешное вычитание — следующая цифра 1)

Наша цифра «10» теперь сдвинута на две позиции вправо, возвращая ее к исходному значению, что является нашим сигналом к ​​остановке и подсчету цифр в нашем ответе -% 101 в двоичном формате или «5» в десятичном, w ich — это, конечно, правильный ответ.

Реализация процедуры в машинном коде

Показав, что у нас есть рабочий алгоритм двоичного деления, теперь нам нужно перевести его в настоящие инструкции ассемблера.Я собираюсь разделить R1 на R2; нам также нужно будет использовать регистры R0 и R4. Прежде чем мы начнем, необходимо выполнить всего одну обязательную проверку . сделал….
 CMP R2, # 0
 BEQ Divide_end
 ; проверьте деление на ноль!
 

Настройка подразделения

Чтобы разделить R1 на R2, первое, что нам нужно сделать, это сдвинуть R2 влево на необходимое количество мест. Самый простой способ сделать это — просто методом проб и ошибок — сдвигать до тех пор, пока мы обнаруживаем, что R2 стал слишком большим, и останавливаемся.
 MOV R0, # 0; очистить R0 для накопления результата
 MOV R3, # 1; установить бит 0 в R3, который будет
                    ; сдвинут влево, затем вправо
.Начало
 CMP R2, R1
 MOVLS R2, R2, LSL №1
 MOVLS R3, R3, LSL # 1
 BLS начало
 ; сдвиньте R2 влево, пока
 ; быть больше R1
 ; сдвиньте R3 влево параллельно по порядку
 ; чтобы отметить, как далеко нам нужно пройти
 

R0 будет использоваться для хранения результата. Роль R3 более сложная.

Фактически, мы используем R3, чтобы отметить, где должен находиться правый конец R2 — если мы сдвинем R2 осталось три места, это будет обозначено значением% 1000 в R3.Однако мы также добавляем его в R0 каждый раз, когда нам удается успешно вычитать, так как он отмечает позицию цифры в настоящее время рассчитывается в ответе. В приведенном выше двоичном примере (50 ÷ 10) мы сдвинул «10» на два места влево, поэтому во время первого вычитания R3 был бы % 100, во время второго (неудачного) было бы% 10, а во время третий% 1. Добавление его к R0 после каждого успешного вычитания снова дало бы нам ответ% 101!

Код ARM не имеет конкретных инструкций сдвига и поворота, присутствующих в не-RISC наборы инструкций.Вместо этого у него есть концепция «барреля-переключателя», который можно использовать для изменить значение, указанное в правом регистре для почти любой инструкции , без изменения самого регистра. Например, инструкция ADD R0, R1, R2, LSL # 2 будет сложите R1 и (R2 << 2) и загрузите результат в R0, не влияя на значение R2 никак. Это может быть очень полезно, но это означает, что если мы действительно хотим изменить значение R2, сдвигая его, как мы это делаем здесь, мы должны прибегнуть к перемещению его в себя через Переключатель: MOV R2, R2, LSL # 1.

Петля вычитания

Теперь о цикле, который действительно выполняет работу:
.следующий
 CMP R1, R2; перенос установлен, если R1> R2 (не спрашивайте почему)
 SUBCS R1, R1, R2; вычтите R2 из R1, если это
                      ; дать положительный ответ
 ADDCS R0, R0, R3; и добавить текущий бит в R3 к
                      ; накапливающийся ответ в R0
 

При вычитании кода ARM (инструкция CMP имитирует вычитание для установки флагов), если R1 — R2 дает положительный ответ и «заимствование» не требуется, флаг переноса равен и установлен .Это необходимо для правильной работы SBC (вычитание с переносом) при использовании для переноски. получается 64-битное вычитание, но это сбивает с толку!

В данном случае мы используем это в своих интересах. Флаг переноса устанавливается, чтобы указать, что возможно успешное вычитание, т. е. такое, которое не дает отрицательного результата, и две следующие инструкции выполняются только при выполнении условия Carry Set. Обратите внимание, что ‘S’ в конце этих инструкций является частью кода условия ‘CS’ и означает , а не . значит, что они устанавливают флаги!

 MOVS R3, R3, LSR # 1; Сдвинуть R3 вправо во флаг переноса
 MOVCC R2, R2, LSR # 1; и если бит 0 R3 был равен нулю, также
                           ; сдвинуть R2 вправо
 BCC следующий; если перенос не очищен, R3 сдвинулся
                           ; назад туда, где все началось, и мы
                           ; может закончиться

.Divide_end
 MOV R25, R24; выход из программы
 

Следующие две инструкции сдвигают вправо R3, регистр «счетчик», и R2, который содержит число, на которое мы делим. Мы специально устанавливаем флаги, используя суффикс «S» при переключении R3, поскольку мы хотим знать, когда бит, хранящийся в этом регистре, достигает правой части. В течение сдвиг вправо, бит 0 передается во флаг переноса, в то время как остальные биты перемещаются вдоль. Поскольку установлен только один бит R3 (изначально он был загружен с% 1 перед сдвигом) влево, а затем вправо), когда установлен флаг переноса, он указывает, что перед сдвигом значение R3 было% 1, т.е.е. мы вернулись туда, откуда начали, и теперь R0 должен держать правую отвечать.

Сводка

В конце подпрограммы R1 сохраняет остаток, если таковой имеется, R2 вернул значение, которое он удерживал. при входе в процедуру R0 сохраняет результат, а R3 сохраняет ноль. И нулевой, и переносящий флаги набор. Эта процедура не работает для отрицательных значений R1 или R2.

Как и в случае с результатами целочисленного деления в Basic, значение в R0 всегда будет округляться до next наименьшее целое число , а не до ближайшего числа.Например, 1156 ÷ 19 дает результат «60 остатков 16», который на самом деле ближе к 61, чем к 60.



2б. BASIC MATH — AVR ASM INTRODUCTION

BASIC AVR ARITHMETIC v1.7


MULTIPLICATION, DIVISION, SQUARE & CUBE ROOT
by [email protected] СОДЕРЖАНИЕ:
  1. УМНОЖЕНИЕ ДВУХ ОДНОБАЙТНЫХ НОМЕРОВ С ПОМОЩЬЮ MUL КОМАНДЫ
  2. УМНОЖЕНИЕ ОДНОБАЙТНОГО НОМЕРА МОЩНОСТЬЮ ДВУХ
  3. УМНОЖЕНИЕ ДВУХ ОДНОБАЙТНЫХ НОМЕРОВ ВРУЧНУЮ
  4. УМНОЖЕНИЕ ДВУХ 16-БИТНЫХ НОМЕРОВ С КОМАНДОЙ MUL
  5. УМНОЖЕНИЕ ДВУХ 16-БИТНЫХ НОМЕРОВ ВРУЧНУЮ
  6. УМНОЖЕНИЕ ДВУХ 32-БИТНЫХ НОМЕРОВ ВРУЧНУЮ
  7. РАЗДЕЛЕНИЕ НА ДВОЙКУ
  8. РАЗДЕЛЕНИЕ ДВУХ ОДНОБАЙТНЫХ НОМЕРОВ
  9. РАЗДЕЛЕНИЕ НА ДВА 16-БИТНЫХ ЧИСЛА
  10. РАЗДЕЛЕНИЕ 32-БИТНОГО НОМЕРА
  11. КВАДРАТНЫЙ КОРНЬ ОДНОБАЙТНОГО ЧИСЛА
  12. Квадратный корень 16-разрядного числа
  13. КВАДРАТНЫЙ КОРН 32-БИТНОГО ЧИСЛА
  14. КУБИЧЕСКИЙ КОРНЕЙ ОДНОБАЙТНОГО НОМЕРА
  15. КУБИЧЕСКИЙ КОРНЕЙ 16-БИТНОГО ЧИСЛА

1.УМНОЖЕНИЕ ДВУХ ОДНОБАЙТНЫХ НОМЕРОВ С ПОМОЩЬЮ MUL КОМАНДЫ

Если ваша микросхема AVR поддерживает команду умножения (MUL), то умножение двух восьмибитных цифры довольно просты. MUL будет работать со всеми 32 регистрами с R0 по R31 и оставит младший байт результата в R0 и старшего байта в R1. Регистры множимого и множителя остаются без изменений. Процедура занимает около трех циклов.

 .DEF ANSL = R0; Удерживать младший байт ответа
.DEF ANSH = R1; Хранить старший байт ответа
.DEF A = ​​R16; удерживать множимое
.DEF B = R18; Удерживать множитель

        LDI A, 42; Загрузить множимое в A
        LDI B, 10; Загрузить множитель в B
        MUL A, B; умножить содержимое A и B
                          ; Результат 420 остался в ANSL и ANSH
 

2. УМНОЖЕНИЕ ОДНОБАЙТНОГО НОМЕРА МОЩНОСТЬЮ ДВУХЧАРОВ

Если наш AVR не поддерживает аппаратную команду MUL, нам придется вычислять умножение вручную. Если нам нужно умножить на степень двойки, например 2,4,8 и т. Д.Результат можно получить, сдвинув биты влево. Каждый сдвиг влево — это умножение на два.

Команда логического сдвига влево (LSL) используется для младшего байта, потому что она сдвиньте содержимое на один бит влево, ноль сдвинется в младший бит и самый старший бит сдвигается во флаг переноса.

 10101010
Перенести [1] <- 01010100 <- 0 (LSL)
 

Мы используем команду «Повернуть влево, хотя переносить» (ROL) на старший байт, потому что она также сдвинет содержимое на один бит влево, но это переместит содержимое флага переноса в самый младший бит.

 00000000
Перенести [0] <- 00000001 <- [1] Перенести (ROL)
 

Каждый раз, когда мы сдвигаем множимое влево, мы умножаем его на два. Итак, чтобы умножить на восемь, мы просто сдвигаем множимое влево три раза. Процедура занимает около десяти циклов.

 .DEF ANSL = R0; Удерживать младший байт ответа
.DEF ANSH = R1; Хранить старший байт ответа
.DEF AL = R16; Для хранения младшего байта умножаемого
.DEF AH = R17; Для хранения старшего байта умножаемого

        LDI AL, LOW (42); Загрузить множимое в AH: AL
        LDI AH, ВЫСОКИЙ (42);
MUL8:
        MOV ANSL, AL; Копировать множимое в R1: R0
        MOV ANSH, AH;
        LSL ANSL; Умножить на 2
        ROL ANSH; Переместите Carry в R1
        LSL ANSL; Умножить на 2x2 = 4
        ROL ANSH; Переместите Carry в R1
        LSL ANSL; Умножить на 2x2x2 = 8
        РОЛ АНШ; Переместите переноску в R1
                          ; Результат 42x8 = 336 осталось в ANSL и ANSH
 

3.УМНОЖЕНИЕ ДВУХ ОДНОБАЙТНЫХ НОМЕРОВ ВРУЧНУЮ

Чтобы выполнить стандартное умножение, мы исследуем, как достигается двоичное умножение, мы замечаем, что когда цифра в множителе равна единице, мы добавляем сдвинутую версию множимое к нашему результату. Когда цифра множителя равна нулю, нам нужно добавить ноль, что означает, что мы ничего не делаем.

 00101010 = 42 множимого
                         x00001010 = 10 множитель
                          --------
                          00000000
                         00101010
                        00000000
                       00101010
                      00000000
                     00000000
                    00000000
                   00000000
                  ----------------
                  0000000110100100 = 420 результат
                  ================
 

Приведенная ниже процедура имитирует аппаратное умножение (MUL), оставляя множимое и множитель не изменяется, и результат появляется в регистровой паре R1 и R0.Он сдвигает биты умножителя в бит переноса и использует содержимое переноса для сложите множимое, если оно равно единице, или пропустите его, если перенос равен нулю. Процедура занимает около шестидесяти циклов.

 .DEF ANSL = R0; Удерживать младший байт ответа
.DEF ANSH = R1; Хранить старший байт ответа
.DEF A = ​​R16; Для удержания множимого
.DEF B = R18; Удерживать множитель
.DEF C = R20; Удерживать битовый счетчик

        LDI A, 42; Загрузить множимое в A
        LDI B, 10; Загрузить множитель в B
MUL8x8:
        LDI C, 8; Загрузить битовый счетчик в C
        CLR ANSH; Очистить старший байт ответа
        MOV ANSL, B; Копировать множитель в младший байт ответа
        LSR ANSL; сдвинуть младший бит множителя в Carry
LOOP: BRCC SKIP; если перенос равен нулю, пропустить сложение
        ADD ANSH, A; Сложить множимое для ответа
SKIP: ROR ANSH; сдвиг младшего бита старшего байта
        ROR ANSL; ответа в младший байт
        DEC C; Уменьшение битового счетчика
         BRNE LOOP; Проверить, все ли восемь бит выполнены
                          ; Результат 420 остался в ANSL и ANSH
 

4.УМНОЖЕНИЕ ДВУХ 16-БИТНЫХ НОМЕРОВ С ПОМОЩЬЮ MUL КОМАНДЫ

Умножение двух 16-битных чисел может дать четырехбайтовый результат. Мы используем команду аппаратного умножения (MUL) для создания всех четырех перекрестных произведений. и добавьте их к 32-битному результату. Команда MUL оставляет свои результаты каждый раз в R1: R0, которые мы затем добавляем к нашему результату. Процедура занимает около двадцати циклов.

 .DEF ZERO = R2; Удерживать ноль
.DEF AL = R16; Для удержания множимого
.DEF AH = R17
.DEF BL = R18; удерживать множитель
.DEF BH = R19
.DEF ANS1 = R20; Для хранения 32-битного ответа
.DEF ANS2 = R21
.DEF ANS3 = R22
.DEF ANS4 = R23

        LDI AL, LOW (42); Загрузить множимое в AH: AL
        LDI AH, ВЫСОКИЙ (42);
        LDI BL, LOW (10); Загрузить множитель в BH: BL
        LDI BH, ВЫСОКИЙ (10);

MUL16x16:
        CLR ZERO; установить R2 в ноль
        MUL AH, BH; Умножить старшие байты AHxBH
        MOVW ANS4: ANS3, R1: R0; Переместить двухбайтовый результат в ответ

        MUL AL, BL; Умножение младших байтов ALxBL
        MOVW ANS2: ANS1, R1: R0; Переместить двухбайтовый результат в ответ

        MUL AH, BL; Умножить AHxBL
        ДОБАВИТЬ ANS2, R0; Добавить результат к ответу
        АЦП ANS3, R1;
        ADC ANS4, ZERO; добавить несущий бит

        MUL BH, AL; Умножить BHxAL
        ДОБАВИТЬ ANS2, R0; Добавить результат к ответу
        АЦП ANS3, R1;
        ADC ANS4, ZERO; добавить несущий бит
 

5.УМНОЖЕНИЕ ДВУХ 16-БИТНЫХ НОМЕРОВ ВРУЧНУЮ

Умножение двухбайтовых чисел вместе может дать результат шириной четыре байта (32 бита). С помощью этой процедуры мы добавляем множимое к старшим байтам нашего результата для каждого, что появляется в нашем 16-битном умножителе, затем сдвинем результат в младшие байты нашего результата шестнадцать раз, один раз для каждого бита нашего множителя. Процедура занимает около 180 циклов.

 .DEF AL = R16; Для удержания множимого
.DEF AH = R17
.DEF BL = R18; удерживать множитель
.DEF BH = R19
.DEF ANS1 = R20; Для хранения 32-битного ответа
.DEF ANS2 = R21
.DEF ANS3 = R22
.DEF ANS4 = R23
.DEF C = R24; Битовый счетчик

        LDI AL, LOW (42); Загрузить множимое в AH: AL
        LDI AH, ВЫСОКИЙ (42);
        LDI BL, LOW (10); Загрузить множитель в BH: BL
        LDI BH, ВЫСОКИЙ (10);

MUL16x16:
        CLR ANS3; Обнулить старшие байты результата
        CLR ANS4;
        LDI C, 16; битовый счетчик
LOOP: LSR BH; множитель сдвига вправо
        ROR BL; сдвинуть самый младший бит в флаг переноса
         BRCC SKIP; если перенос равен нулю, пропустить добавление
        ADD ANS3, AL; Добавить множимое в старшие байты
        ADC ANS4, AH; результата
SKIP: ROR ANS4; Повернуть старшие байты результата в
        ROR ANS3; младшие байты
        ROR ANS2;
        ROR ANS1;
        DEC C; Проверить, все ли 16 бит обработаны
         BRNE LOOP; Если нет, то вернитесь назад
 

6.УМНОЖЕНИЕ ДВУХ 32-БИТНЫХ НОМЕРОВ ВРУЧНУЮ

Следующая процедура умножит два 32-битных числа на 64-битный (8-байтовый) результат. Процедура занимает около 500 тактов.

 .DEF ANS1 = R0; 64-битный ответ
 .DEF ANS2 = R1;
 .DEF ANS3 = R2;
 .DEF ANS4 = R3;
 .DEF ANS5 = R4;
 .DEF ANS6 = R5;
 .DEF ANS7 = R6;
 .DEF ANS8 = R7;
 .DEF A1 = R16; Множаемое
 .DEF A2 = R17;
 .DEF A3 = R18;
 .DEF A4 = R19;
 .DEF B1 = R20; Множитель
 .DEF B2 = R21;
 .DEF B3 = R22;
 .DEF B4 = R23;
 .DEF C = R24; Счетчик циклов
        
        LDI A1, НИЗКИЙ ($ FFFFFFFF)
        LDI A2, BYTE2 ($ FFFFFFFF)
        LDI A3, BYTE3 ($ FFFFFFFF)
        LDI A4, BYTE4 ($ FFFFFFFF)
        LDI B1, НИЗКИЙ ($ FFFFFFFF)
        LDI B2, BYTE2 ($ FFFFFFFF)
        LDI B3, BYTE3 ($ FFFFFFFF)
        LDI B4, BYTE4 ($ FFFFFFFF)
MUL3232:
        CLR ANS1; инициализировать ответ нулем
        CLR ANS2;
        CLR ANS3;
        CLR ANS4;
        CLR ANS5;
        CLR ANS6;
        CLR ANS7;
        SUB ANS8, ANS8; очистить ANS8 и флаг переноса
        MOV ANS1, B1; Копировать множитель для ответа
        MOV ANS2, B2;
        MOV ANS3, B3;
        MOV ANS4, B4;
        LDI C, 33; Установить счетчик циклов на 33
ПЕТЛЯ:
        ROR ANS4; множитель сдвига вправо
        ROR ANS3;
        ROR ANS2;
        ROR ANS1;
        DEC C; счетчик цикла уменьшения
         BREQ DONE; Проверить, все ли биты обработаны
          BRCC SKIP; If Carry Clear пропустить добавление
        ДОБАВИТЬ ANS5, A1; Добавить мультипис. В ответ
        АЦП ANS6, A2;
        АЦП ANS7, A3;
        АЦП ANS8, A4;
ПРОПУСКАТЬ:
        ROR ANS8; сдвиг старших байтов ответа
        ROR ANS7;
        ROR ANS6;
        ROR ANS5;
         ПЕТЛЯ RJMP
ВЫПОЛНЕНО:
 

7.РАЗДЕЛЕНИЕ НА ДВУМЯ

Поскольку команды аппаратного разделения нет, нам придется делать это вручную. Если нам нужно разделить на степень двойки, например 2,4,8 и т. Д. Результат может быть достигнут сдвигом битов вправо. Каждый сдвиг вправо - это деление на два.

Команда логического сдвига вправо (LSR) используется для старшего байта, потому что она сдвигает содержимое на один бит вправо, ноль сдвигается в старший бит, а младший бит сдвигается во флаг переноса.

 01010101
         0 -> 00101010 -> [1] Перенести
 

Мы используем команду «Повернуть вправо, хотя переносить» (ROR) для младшего байта, потому что она также сдвинет содержимое на один бит вправо, но это переместит содержимое флага переноса в самый старший бит.

 00000000
Перенести [1] -> 10000000 -> [0] Перенести (ROL)
 

Каждый раз, когда мы сдвигаем множимое вправо, мы делим его на два. Итак, чтобы разделить на восемь, мы просто сдвигаем множимое вправо три раза.Процедура занимает около десяти циклов.

 .DEF ANSL = R0; Удерживать младший байт ответа
.DEF ANSH = R1; Хранить старший байт ответа
.DEF AL = R16; Для хранения младшего байта умножаемого
.DEF AH = R17; Для хранения старшего байта умножаемого

        LDI AL, LOW (416); Загрузить множимое в A
        LDI AH, ВЫСОКИЙ (416);
DIV8:
        MOVW ANSH: ANSL, AH: AL; Копировать множимое в результат
        ЛСР АНШ; Разделить на 2
        ROR ANSL; Сдвинуть флаг переноса на R0
        ЛСР АНШ; Разделить на 4 (2x2)
        ROR ANSL; Сдвинуть флаг переноса в R0
        ЛСР АНШ; Разделить на 8 (2x2x2)
        ROR ANSL; Сдвинуть флаг переноса в R0
                          ; Результат 416/8 = 52 осталось в ANSL и ANSH
 

8.РАЗДЕЛЕНИЕ ДВУХ ОДНОБАЙТНЫХ НОМЕРОВ

Точно так же, как умножение может быть достигнуто с помощью сдвига и сложения. Деление можно осуществить сдвигом и вычитанием. Приведенная ниже процедура пытается многократно вычесть делитель. Если результат отрицательный, процесс отменяется, и дивиденды сдвигаются влево, чтобы повторить попытку. Процедура занимает около 90 циклов.

 .DEF ANS = R0; Удерживать ответ
.DEF REM = R2; Для удержания остатка
.DEF A = ​​R16; Держать дивиденды
.DEF B = R18; удерживать делитель
.DEF C = R20; Битовый счетчик

        LDI A, 255; Загрузить делимое в A
        LDI B, 5; Делитель нагрузки в B
DIV88:
        LDI C, 9; счетчик битов загрузки
        SUB REM, REM; Очистить остаток и перенести
        MOV ANS, A; Копировать дивиденд в ответ
LOOP: ROL ANS; сдвиньте ответ влево
        DEC C; счетчик уменьшения
         BREQ DONE; выйти, если выполнено восемь бит
        ROL REM; сдвинуть остаток влево
        SUB REM, B; Попробуйте вычесть делитель из остатка
         BRCC SKIP; Если результат отрицательный, то
        ADD REM, B; обратное вычитание, чтобы повторить попытку
        CLC; Очистить флаг переноса, чтобы ноль сместился в A
         RJMP LOOP; обратная петля
SKIP: SEC; Установить флаг переноса на A
         ПЕТЛЯ RJMP
ВЫПОЛНЕНО:
 

9.РАЗДЕЛЕНИЕ НА ДВА 16-БИТНЫХ ЧИСЛА

Предыдущая подпрограмма может быть расширена для обработки двухбайтовых чисел в диапазоне от нуля до 65 535. Процедура занимает около 230 циклов.

 .DEF ANSL = R0; Удерживать младший байт ответа
.DEF ANSH = R1; Хранить старший байт ответа
.DEF REML = R2; Для хранения младшего байта остатка
.DEF REMH = R3; Для хранения старшего байта остатка
.DEF AL = R16; Для хранения младшего байта дивиденда
.DEF AH = R17; Хранить старший байт дивиденда
.DEF BL = R18; Для хранения младшего байта делителя
.DEF BH = R19; Для хранения старшего байта делителя
.DEF C = R20; Битовый счетчик

        LDI AL, LOW (420); Загрузить младший байт делимого в AL
        LDI AH, HIGH (420); Загрузить старший байт делимого в AH
        LDI BL, LOW (10); Загрузить младший байт делителя в BL
        LDI BH, HIGH (10); Загрузить старший байт делителя в BH
DIV1616:
        MOVW ANSH: ANSL, AH: AL; Скопировать делимое в ответ
        LDI C, 17; битовый счетчик нагрузки
        SUB REML, REML; очистить остаток и перенести
        CLR REMH;
LOOP: ROL ANSL; сдвиньте ответ влево
        РОЛ АНШ;
        DEC C; счетчик уменьшения
         BREQ DONE; выйти, если выполнено шестнадцать бит
        ROL REML; Сдвинуть остаток влево
        ROL REMH;
        SUB REML, BL; Попробуйте вычесть делитель из остатка
        SBC REMH, BH
         BRCC SKIP; Если результат отрицательный, то
        ADD REML, BL; отмените вычитание, чтобы повторить попытку
        ADC REMH, BH;
        CLC; Очистить флаг переноса, чтобы ноль сместился в A
         RJMP LOOP; обратная петля
SKIP: SEC; Установить флаг переноса на A
         ПЕТЛЯ RJMP
ВЫПОЛНЕНО:
 

10.РАЗДЕЛЕНИЕ 32-БИТНОГО НОМЕРА

Предыдущая подпрограмма может быть расширена для обработки 32-битного числа, деленного на 16-битное число, Это означает, что числа в диапазоне от нуля до 4 294 967 295 (4,3 миллиарда) разделены на числа в диапазоне от нуля до 65 535. Процедура занимает около 700 циклов.

 .DEF ANS1 = R0; Удерживать младший байт ответа
.DEF ANS2 = R1; Для хранения второго байта ответа
.DEF ANS3 = R2; Для хранения третьего байта ответа
.DEF ANS4 = R3; Для хранения четвертого байта ответа

.DEF REM1 = R4; для хранения первого байта остатка
.DEF REM2 = R5; Для хранения второго байта остатка
.DEF REM3 = R6; Для хранения третьего байта остатка
.DEF REM4 = R7; Для хранения четвертого байта остатка

.DEF ZERO = R8; Удерживать нулевое значение

.DEF A1 = R16; Для хранения младшего байта дивиденда
.DEF A2 = R17; Для хранения второго байта делимого
.DEF A3 = R18; Для хранения третьего байта дивиденда
.DEF A4 = R19; Для хранения четвертого байта делимого

.DEF BL = R20; Для хранения младшего байта делителя
.DEF BH = R21; Для хранения старшего байта делителя

.DEF C = R22; Битовый счетчик

        LDI A1, LOW (420); Загрузить младший байт делимого в A1
        LDI A2, BYTE2 (420); Загрузить второй байт делимого в A2
        LDI A3, BYTE3 (420); Загрузить третий байт делимого в A3
        LDI A4, BYTE4 (420); загрузить четвертый байт делимого в A4

        LDI BL, LOW (10); Загрузить младший байт делителя в BL
        LDI BH, HIGH (10); Загрузить старший байт делителя в BH

DIV3216:
        CLR ZERO
        MOVW ANS2: ANS1, A2: A1; Скопировать делимое в ответ
        MOVW ANS4: ANS3, A4: A3;
        LDI C, 33; битовый счетчик нагрузки
        SUB REM1, REM1; очистить остаток и перенести
        CLR REM2;
        CLR REM3;
        CLR REM4;
LOOP: ROL ANS1; сдвиньте ответ влево
        ROL ANS2;
        ROL ANS3;
        ROL ANS4;
        DEC C; счетчик уменьшения
         BREQ DONE; выйти, если выполнено 32 бита
        ROL REM1; Сдвинуть остаток влево
        ROL REM2;
        ROL REM3;
        ROL REM4;
        SUB REM1, BL; Попробуйте вычесть делитель из остатка
        SBC REM2, BH;
        SBC REM3, ZERO;
        SBC REM4, ZERO;
         BRCC SKIP; Если результат отрицательный, то
        ADD REM1, BL; отмените вычитание, чтобы повторить попытку
        ADC REM2, BH;
        АЦП REM3, НУЛЬ;
        АЦП REM4, НУЛЬ;
        CLC; Очистить флаг переноса, чтобы ноль сместился в A
         RJMP LOOP; обратная петля
SKIP: SEC; Установить флаг переноса на A
         ПЕТЛЯ RJMP
ВЫПОЛНЕНО:
 

11.2 = 25

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

 .DEF ANS = R0; Удерживать ответ
.DEF A = ​​R16; Удерживать квадрат
.DEF B = R18; Сумма, Рабочее пространство.

  LDI A, 100; Загрузите квадрат в A

КОРЕНЬ:
ПЕТЛЯ:
  SUB A, B; вычесть B из квадрата
   BRCS DONE; Если больше, чем sqaure, мы закончили
  INC ANS; Увеличить ответ
  SUBI B, -2; Приращение B на два
   ПЕТЛЯ RJMP
 

12.Квадратный корень 16-разрядного числа

Мы могли бы расширить описанную выше процедуру для обработки квадратного корня из шестнадцатибитного числа. но количество тактовых циклов может достигать 3750. Приведенная ниже процедура обрабатывает два бита за раз, начиная с самых высоких битов и также предоставляет остаток. Он использует около 160 тактов.

 .DEF ANSL = R0; Квадратный корень (ответ)
 .DEF ANSH = R1;
 .DEF REML = R2; Остаток
 .DEF REMH = R3;
 .DEF AL = R16; Квадрат для укоренения (ввод)
 .DEF AH = R17;
 .DEF C = R20; Счетчик циклов
        
        LDI AL, НИЗКИЙ ($ FFFF)
        LDI AH, ВЫСОКИЙ ($ FFFF)
SQRT16:
        PUSH AL; Сохранить площадь для последующего восстановления
        PUSH AH;
        CLR REML; инициализировать остаток до нуля
        CLR REMH;
        CLR ANSL; Инициализировать корень до нуля
        CLR ANSH;
        LDI C, 8; установить счетчик циклов на восемь
ПЕТЛЯ:
        LSL ANSL; Умножить корень на два
        РОЛ АНШ;
        LSL AL; сдвиг на два старших бита квадрата
        ROL AH; в остаток
        ROL REML;
        ROL REMH;
        LSL AL; сдвиг второго старшего бита Sqaure
        ROL AH; в остаток
        ROL REML;
        ROL REMH;
        CP ANSL, REML; Сравнить корень с остатком
        CPC ANSH, REMH;
         BRCC SKIP; если остаток меньше или равен корневому
        INC ANSL; Приращение корня
        SUB REML, ANSL; вычесть корень из остатка
        SBC REMH, ANSH;
        INC ANSL; Приращение корня
ПРОПУСКАТЬ:
        DEC C; счетчик цикла уменьшения
         BRNE LOOP; Проверить, все ли биты обработаны
        LSR ANSH; Разделить корень на два
        ROR ANSL;
        POP AH; Восстановить исходную площадь
        ПОП АЛЬ
   ПЕТЛЯ RJMP
 

13.Квадратный корень 32-разрядного числа

Мы можем расширить предыдущую процедуру для обработки 32-битных чисел. Для завершения требуется от 500 до 580 тактов.

 .DEF ANS1 = R0; Квадратный корень (ответ)
 .DEF ANS2 = R1;
 .DEF ANS3 = R2;
 .DEF ANS4 = R3;
 .DEF REM1 = R4; Остаток
 .DEF REM2 = R5;
 .DEF REM3 = R6;
 .DEF REM4 = R7;
 .DEF A1 = R16; Квадрат (ввод)
 .DEF A2 = R17;
 .DEF A3 = R18;
 .DEF A4 = R19;
 .DEF C = R20; Счетчик циклов
        
        LDI A1, НИЗКИЙ ($ FFFFFFFF)
        LDI A2, BYTE2 ($ FFFFFFFF)
        LDI A3, BYTE3 ($ FFFFFFFF)
        LDI A4, BYTE4 ($ FFFFFFFF)
sqrt16:
        НАЖМИТЕ A1; Сохранить квадрат для последующего восстановления
        PUSH A2;
        НАЖАТЬ A3;
        PUSH A4;
        CLR REM1; инициализировать остаток до нуля
        CLR REM2;
        CLR REM3;
        CLR REM4;
        CLR ANS1; инициализировать корень до нуля
        CLR ANS2;
        CLR ANS3;
        CLR ANS4;
        LDI C, 16; Установить счетчик циклов на шестнадцать
ПЕТЛЯ:
        LSL ANS1; Умножить корень на два
        ROL ANS2;
        ROL ANS3;
        ROL ANS4;
        LSL A1; сдвиг на два старших бита квадрата
        ROL A2; в остаток
        ROL A3;
        ROL A4;
        ROL REM1;
        ROL REM2;
        ROL REM3;
        ROL REM3;
        LSL A1; сдвиг второго старшего бита Sqaure
        ROL A2; в остаток
        ROL A3;
        ROL A4;
        ROL REM1;
        ROL REM2;
        ROL REM3;
        ROL REM4;
        CP ANS1, REM1; Сравнить корень с остатком
        КТК ANS2, REM2;
        КТК ANS3, REM3;
        КТК ANS4, REM4;
         BRCC SKIP; если остаток меньше или равен корневому
        INC ANS1; Увеличение корня
        SUB REM1, ANS1; вычесть корень из остатка
        SBC REM2, ANS2;
        SBC REM3, ANS3;
        SBC REM4, ANS4;
        INC ANS1; Увеличение корня
ПРОПУСКАТЬ:
        DEC C; счетчик цикла уменьшения
         BRNE LOOP; Проверить, все ли биты обработаны
        LSR ANS4; Разделить корень на два
        ROR ANS3;
        ROR ANS2;
        ROR ANS1;
        POP A4; Восстановить исходную площадь
        POP A3
        POP A2
        POP A1
 

14. 3 MUL C, R0; CP A, R0; Проверить, не зашли ли слишком далеко BRCS FINI; Если да, то готово MOV REM, A; вычислить остаток SUB REM, R0; INC C; счетчик цикла приращения CPI C, 7; Проверить, выполнено ли BRNE LOOP; Вернуться назад FINI: MOV ANS, C; Ответ = Счетчик - 1 DEC ANS

15.3 .DEF REMH = R3; .DEF CUB1 = R4; Ответ из процедуры CUBE .DEF CUB2 = R5; .DEF CUB3 = R6; .DEF TMP1 = R8; Временное рабочее пространство .DEF TMP2 = R9; .DEF ZERO = R10; Удерживать нулевое значение .DEF ONE = R11; Для хранения значения One .DEF LES = R12; Низкая оценка .DEF HES = R13; Высокая оценка .DEF AVG = R14; Средняя минимальная и максимальная оценка .DEF AL = R16; Исходный куб .DEF AH = R17; .DEF B = R18; общего назначения LDI AL, LOW (1000); Загрузить исходный куб в AH: AL LDI AH, ВЫСОКИЙ (1000); CLR REML; Очистить остаток CLR REMH; CLR ZERO; Установить ноль CLR ONE; установить один INC ONE; CLR LES; Начать низкую оценку с нуля LDI B, 42; начальная высокая оценка с сорока двух MOV HES, B ЦИКЛ: MOV AVG, LES; AVG = (LowEst + HighEst + 1) / 2 ДОБАВИТЬ AVG, HES; ADC AVG, ONE; ЛСР AVG; MOV B, AVG; Вычислить AVG ^ 3 RCALL CUBE; CP CUB1, AL; Сравнить AVG ^ 3 с исходным CUBE CPC CUB2, AH; CPC CUB3, ZERO; BRNE SKIP1; Проверить, если AVG ^ 3 = Original CUBE MOV LES, AVG; AVG ^ 3 = CUBE, поэтому Low-Est = AVG (завершено) RJMP FINI2; SKIP1: BRCC ISHIGH; MOV LES, AVG; AVG ^ 3

CUBE, поэтому High-Est = AVG SKIP2: MOV B, HES; B = HighEst — LowEst SUB B, LES; BREQ FINI; LowEst = HighEst, поэтому мы закончили ИПЦ B, 1; BRNE LOOP; Если HighEst-LowEst> 1, попробуйте еще раз ФИНИ: MOV B, LES; Вычислить остаток RCALL CUBE; Остаток = Cube — LowEst ^ 3 MOVW REMH: REML, AH: AL; SUB REML, CUB1; SBC REMH, CUB2; FINI2: MOV ANS, LES; сохранить результат = LowEst СДЕЛАНО: RJMP СДЕЛАНО ; —————————-; ; Рассчитывает куб B; ; Результаты в CUB3: CUB2: CUB1; ; —————————-; КУБ: MUL B, B; Расчет B * B MOVW TMP2: TMP1, R1: R0; MUL TMP1, B; Расчет B * B * B MOVW CUB2: CUB1, R1: R0; MUL TMP2, B; ДОБАВИТЬ CUB2, R0; CLR CUB3; ADC CUB3, R1; RET

Язык ассемблера CSC233 — умножение и деление

Язык ассемблера CSC233 — умножение и деление

Умножение и деление

Умножение

Команды M (RX) и MR (RR) выполняют умножение двоичных полных слов операндов.Инструкции умножения использовать четно-нечетную регистровую пару для хранения первого операнда и продукта. Пара четно-нечетных регистров пара последовательных регистров, которая начинается с четного регистра, например регистры 2 и 3, регистры 6 и 7 или регистры 10 и 11.

Чтобы произвести умножение, множимое загружается в нечетный регистр четно-нечетной пары. Чётный регистр — это один, указанный в инструкции умножения. После умножения продукт находится в четно-нечетной паре.Если вы знаете, что продукт подойдет один регистр, тогда он будет в нечетном регистре пары.

Дивизия

Команды D (RX) и DR (RR) выполняют деление двоичных полных слов операндов. В инструкциях по разделению используется четно-нечетная регистровая пара для хранения первого операнда, частного и остатка.

Для выполнения деления делимое загружается в нечетный регистр четно-нечетной пары. Чётный регистр также должен быть инициализирован; если дивиденд положительный, в четном регистре должны быть установлены двоичные нули, и если делимое отрицательное значение, четный регистр должен быть установлен в двоичном формате.В стандартный способ сделать это — загрузить дивиденд, а затем умножить его на один, чтобы сделать делимое 8-байтовым числом. Чётный регистр — это один, упомянутый в инструкции разделения. После разделения нечетный регистр содержит частное, а четный регистр содержит остаток.

Примеры

Версия C ++ Версия ассемблера
А = В * С; L 5, B
M 4, C
ST 5, A
А = В * С; L 7, B
L 11, C
MR 6,11
ST 7, A
А = В / С; L 7, B
M 6, = F’1 ‘
D 6, C
ST 7, A
А = В / С; L 11, B
L 3, C
M 10, = F’1 ‘
DR 10,3
ST 11, A
A = B% C; L 3, B
M 2, = F’1 ‘
D 2, C
ST 2, A
A = B% C; L 9, B
L 7, C
M 8, = F’1 ‘
DR 8,7
ST 8, A

Напишите мне | Часы работы офиса | Моя домашняя страница | Департамент Главная | Домашняя страница MCC

© Авторское право Emmi Schatz 2003

RISC-V Instruction Set Manual, Volume I: RISC-V User-Level ISA

В этой главе описывается стандартное умножение целых чисел и расширение инструкции деления, которое называется «M» и содержит инструкции, которые умножают или делят значения, содержащиеся в двух целых числах регистры.

Операции умножения

MUL выполняет умножение XLEN-бит × XLEN-бит RS1 на RS2 и размещает младшие биты XLEN в регистре назначения. МУЛ, МУЛХУ и МУЛХУ выполнить то же умножение, но вернуть старшие биты XLEN полный 2 × XLEN-битный продукт, для подписанных × подписанных, беззнаковый × беззнаковый и подписанный rs1 × беззнаковый rs2 умножение, соответственно. Если старшие и младшие биты одного и того же продукта равны требуется, то рекомендуемая кодовая последовательность: MULH [[S] U] rdh, RS1, RS2 ; MUL rdl, rs1, rs2 (спецификаторы регистра источника должны быть в том же порядке и rdh не может быть таким же, как rs1 или RS2 ).Микроархитектуры могут затем объединить их в единое целое. операция умножения вместо выполнения двух отдельных операций умножения.

MULW — это инструкция RV64, которая умножает младшие 32 бита источника. регистры, помещая знаковое расширение младших 32 бит результата в регистр назначения.

Оперативная дивизия

DIV и DIVU выполняют биты XLEN на биты XLEN целое число со знаком и без знака деление RS1 на RS2 с округлением до нуля.REM и REMU обеспечивают оставшуюся часть соответствующей операции деления. Для REM знак результата равен знаку дивиденда.

Если и частное, и остаток требуются из того же подразделения, рекомендуемая кодовая последовательность: DIV [U] rdq, RS1, RS2 ; REM [U] rdr, rs1, rs2 ( rdq не может быть таким же, как RS1 или RS2 ). Микроархитектуры могут затем объедините их в одну операцию разделения вместо выполнения два отдельных дивизиона.

DIVW и DIVUW — это инструкции RV64, которые делят младшие 32 бита RS1 младшими 32 битами RS2 , обрабатывая их как целые числа со знаком и без знака соответственно, помещая 32-битный частное в rd , расширенное знаком до 64 бит. REMW и REMUW инструкции RV64, которые предоставляют соответствующие знаковые и беззнаковые операции остатка соответственно. И REMW, и REMUW всегда расширяет 32-битный результат до 64 бит, в том числе на делим на ноль.

Семантика деления на ноль и переполнения при делении кратко изложена в Таблица [вкладка: divby0]. Для частного деления на ноль установлены все биты, и остаток от деления на ноль равен дивиденду. Переполнение подписанного деления происходит только тогда, когда самое отрицательное целое число делится на — 1. Частное от знаковое деление с переполнением равно дивиденду, а остаток равен нуль. Переполнение беззнакового деления не может произойти.

Деление на ноль x 0 2 L — 1 x — 1 x
Переполнение (только с подписью) — 2 L — 1 — 1 — 2 L — 1 0

Шаг за шагом, как сложить / вычесть / умножить / разделить на языке ассемблера.

Сложение + вычитание + умножение + деление на языке ассемблера x86

С возвращением,

Прежде чем мы начнем, я хочу сказать вам, что мы используем библиотеки Irvine и процессор 86x для выполнения таких операций, как упомянуто выше!

Вещи, которые обсуждаются ниже в этом содержании, перечислены ниже:

  • Дополнение на ассемблере x86 => 8086
  • Вычитание на ассемблере x86 => 8086
  • Умножение на ассемблере x86 => 8086
  • Разделение на ассемблере x86 => 8086

Я использую Irvine Library в Visual Studio 2010 Professional для запуска ассемблера для выполнения моего кода!

ДЛЯ ЛУЧШЕГО ПОНИМАНИЯ:

«Процессор имеет только регистрацию в использовании i.е. eax , который используется для отображения, чтобы вы могли хранить данные в других регистрах, но не забудьте поместить свой окончательный результат в регистр eax , чтобы ваша программа не содержала ошибок, вся эта игра сборки передает значения в свободный регистр и правый регистр, где eax — аккумулятор «

»
Дополнение в коде языка ассемблера:

Предположим, значение: 8 + 2 => 10

Вычитание в коде языка ассемблера:

Предположим, значение: 8-2 => 10

Умножение в коде языка ассемблера:

Предположим, значение: 8 * 2 => 10

Код языка ассемблера:

Сложение + умножение и деление вместе, фокус «ПРОХОДНЫЕ ЗНАЧЕНИЯ»

Предположим, уравнение: (3 * 4) + (6/2) + (5 * 2) = 25

.

mov 3 в eax и mov 4 в ebx и используйте mul ebx, чтобы поместить 12 в eax

теперь переместите eax в ecx, как показано в коде!

mov 6 в eax и mov 2 в ebx и div ebx, чтобы поместить ответ в eax

теперь используйте add ecx, eax означает, что ответ 3 * 4 + 6/2 находится в регистре ecx

eax свободен, теперь поместите 5 в eax и снова 2 в ebx!

снова используйте mul ebx и добавьте eax, равный 5 * 2 = 10, в ecx, чтобы получить ответ, равный 25!

.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *