簡體   English   中英

沒有32位寄存器的32位/ 16位有符號整數除法?

[英]32-bit / 16-bit signed integer division without 32-bit registers?

我試圖將一個32位有符號整數除以一個16位有符號整數,得到一個帶符號的32位商和16位余數。

我的目標是沒有fpu的286。

我已經編寫過代碼來執行32位無符號除法:

DIV32 PROC

;DIVIDES A 32-BIT VALUE BY A 16-BIT VALUE.

;ALTERS AX
;ALTERS BX
;ALTERS DX

;EXPECTS THE 32-BIT DIVIDEND IN DX:AX
;EXPECTS THE 16-BIT DIVISOR IN BX

;RETURNS THE 32-BIT QUOTIENT IN DX:AX
;RETURNS THE 16-BIT REMAINDER IN BX

    push di
    push si


    mov di, ax ;di -> copy of LSW of given dividend
    mov ax, dx ;ax -> MSW of given dividend
    xor dx, dx ;dx:ax -> 0:MSW  
    div bx     ;ax:dx -> ax=MSW of final quotient, dx=remainder

    mov si, ax ;si -> MSW of final quotient
    mov ax, di ;dx:ax -> dx=previous remainder, ax=LSW of given dividend
    div bx     ;ax:dx -> ax=LSW of final quotient, dx=final remainder  
    mov bx, dx ;bx -> final remainder
    mov dx, si ;dx:ax -> final quotient


    pop si
    pop di
    ret

DIV32 ENDP 

到目前為止,我已經嘗試做了顯而易見的事情,只需通過交換帶有IDIV XOR DX, DXCWDDIV XOR DX, DX來修改我現有的代碼:

IDIV32 PROC

;DIVIDES A SIGNED 32-BIT VALUE BY A SIGNED 16-BIT VALUE.

;ALTERS AX
;ALTERS BX
;ALTERS DX

;EXPECTS THE SIGNED 32-BIT DIVIDEND IN DX:AX
;EXPECTS THE SIGNED 16-BIT DIVISOR IN BX

;RETURNS THE SIGNED 32-BIT QUOTIENT IN DX:AX
;RETURNS THE 16-BIT REMAINDER IN BX

    push di
    push si


    mov di, ax ;di -> copy of LSW of given dividend
    mov ax, dx ;ax -> MSW of given dividend
    cwd        ;dx:ax -> 0:MSW, or ffff:MSW  
    idiv bx    ;ax:dx -> ax=MSW of final quotient, dx=remainder

    mov si, ax ;si -> MSW of final quotient
    mov ax, di ;dx:ax -> dx=previous remainder, ax=LSW of given dividend
    idiv bx    ;ax:dx -> ax=LSW of final quotient, dx=final remainder  
    mov bx, dx ;bx -> final remainder
    mov dx, si ;dx:ax -> final quotient


    pop si
    pop di
    ret

IDIV32 ENDP 

這適用於某些計算,例如-654,328 / 2 = -327164(0xfff60408 / 2 = fffb0204)。 但它不適用於某些輸入,-131,076 / 2返回-2余數0的錯誤結果。除數為1或-1會導致除法錯誤,看起來不管紅利。

我已經測試了許多不同的正面和負面紅利和除數,試圖找到某種正確和不正確結果的模式,我注意到它無法正確返回-65538的結果。

我有預感,我應該根據輸入有條件地使用CWD ,但看起來像XOR DX, DX更經常地返回不正確的結果。 當除數和被除數都為正且商數小於0x7fffffff時,兩者都可以工作。

我不知道任何將大的負數分成幾部分並為IDIV准備它的算法。 我將計算被除數和除數的絕對值,使用函數DIV32並最后根據存儲的符號處理結果:

IDIV32 PROC      ; DX:AX / BX = DX/AX rem BX
    ; 99 / 5   =  19 rem 4
    ; 99 / -5  = -19 rem 4
    ; -99 / 5  = -19 rem -4
    ; -99 / -5 =  19 rem -4

    mov ch, dh          ; Only the sign bit counts!
    shr ch, 7           ; CH=1 means negative dividend
    mov cl, bh          ; Only the sign bit counts!
    shr cl, 7           ; CL=1 means negative divisor

    cmp ch, 1           ; DX:AX negative?
    jne J1              ; No: Skip the next two lines
    not dx              ; Yes: Negate DX:AX
    neg ax              ; CY=0 -> AX was NULL
    cmc
    adc dx, 0           ; Adjust DX, if AX was NULL
    J1:

    cmp cl, 1           ; BX negative?
    jne J2              ; No: Skip the next line
    neg bx              ; Yes: Negate BX
    J2:

    push cx             ; Preserve CX
    call DIV32
    pop cx              ; Restore CX

    cmp ch, cl          ; Had dividend and divisor the same sign?
    je J3               ; Yes: Skip the next two lines
    not dx
    neg ax              ; CY=0 -> AX was NULL
    cmc
    adc dx, 0           ; Adjust DX, if AX was NULL
    J3:

    cmp ch, 1           ; Was divisor negative?
    jnz J4              ; No: Skip the next line
    neg bx              ; Negate remainder
    J4:

    ret
IDIV32 ENDP

您的算法無法簡單地更改為簽名。

我們以計算(+1)/( - 1)為例:

(+1)/( - 1)=( - 1),余數為0

在算法的第一步中,您將高位除以除數:

(+1)的高位為0,因此您正在計算:

0 /( - 1)= 0,余數為0

然而,整個32位除法的正確高位是0FFFFh,而不是0.並且第二次除法所需的提醒也是0FFFFh而不是0。

哦,所以第二個IDIV應該是DIV。 好的,我明天醒來時會測試它。 如果我讓它工作,我會添加一個答案。

第一個分部已經沒有產生你想要的結果。 所以改變第二師不會有多大幫助......

除數為1或-1會導致除法錯誤,而不管紅利如何。

只有當股息的第15位被設定時,我才會期待這一點:

  • ...你除以1或
  • ...除以-1並且至少設置了一個低15位的被除數

在這些情況下,你要划分:

  • ... 000008000h ... 00000FFFFh范圍內的數字1
    結果將在+ 08000h ... + 0FFFFh范圍內
  • ... 000008001h ... 00000FFFFh范圍內的數字-1
    結果將在-0FFFFh ...- 08001h的范圍內

...但是,結果是帶符號的16位值,因此必須在-8000h ... + 7FFFh范圍內。

我剛剛在運行DOS的虛擬機中嘗試了12345678h /(+ 1)和12345678h /( - 1):

未設置12345678h的第15位; 兩次我都沒有得到除法錯誤。 (但除以-1時錯誤的結果!)

使用2x idiv有一個基本問題 :我們需要第二個除法產生商的低半部分,它是無符號的,可以是0到0xffff之間的任何值。

只有多字整數的最高字包含符號位,下面的所有位都具有正的位值。 idiv的商范圍是-2^15 .. 2^15-1 ,而不是0 .. 65535 是的, idiv可以產生所有必需的值,但不是來自我們可以從第一個分區結果的簡單修正中得到的輸入。 例如, 0:ffff / 1將導致帶有idiv的#DE異常,因為商不適合帶符號的 16位整數。

因此,第二個除法指令必須div使用除數的絕對值和適當的高半。 div要求它的兩個輸入都是無符號的,所以來自第一個idiv帶符號的余數也是一個問題。)

可能仍然可以使用idiv作為第一個除法,但只有在div之前的結果的修正,除了仍然必須取除數的絕對值和第一個余數來提供無符號的div 這是一個有趣的可能性,但在實踐中,保存並重新應用未簽名部門的標志會更便宜。

正如@Martin指出的那樣,帶有幼稚idiv+1 / -1的第一個分區給出了錯誤的高半商(0 / -1 = 0不是-1),而第2個分區的錯誤輸入(0​​%-1 = 0,不是-1)。 TODO:弄清楚實際需要什么樣的修正。 也許只是一個條件+ -1,但我們知道余數的大小不能大於除數,因為high_half < divisor是必要的並且足夠div而不是故障。

你的-131,076 / 2 = -2(可能是巧合)只在其結果的一半中偏離1:
它應該是0xfffefffe = -2:-2而不是-1:-2。


優化版@ rkhb的功能,內聯DIV32。

我們記錄輸入符號,然后對絕對值進行無符號除法,稍后恢復符號。 (如果不需要余數符號,我們可以簡化;商符號僅取決於xor dividend,divisor

或者如果股息足夠小,我們可以使用一個idiv 我們必須避免-2^15 / -1溢出情況,因此快速檢查DX是AX的符號擴展不僅錯過了一些安全情況(具有更大的除數),而是嘗試不安全的情況。 如果小數是常見的(就像大多數計算機程序中的情況一樣),基於cwd這種快速測試仍然是個好主意,在絕對值計算之后進行另一次測試。

分支在286上很便宜,所以我主要保留分支而不是使用無分支abs() (例如,對於單個寄存器:使用cdq(或sar reg,15 )/ xor / sub, 就像編譯器一樣 ,基於2的補碼標識-x = ~x + 1 )。 當然,在P6系列之前, mov / neg / cmovl不可用。 如果您需要使用286,但主要關注現代CPU的性能,您可能會做出不同的選擇。 但事實證明,32位無分支ABS的代碼大小比分支小。 但是,對於正輸入而言,它可能比分支更慢,其中一些指令將被跳過。 匯編程序8086除以16位數字的32位數字有一個有趣的想法,即為被除數和除數創建0 / -1整數,然后為了以后重新應用這些符號你可以將它們一起異或並使用相同的XOR / SUB 2的補碼bithack簽名翻轉結果與否。

樣式:本地標簽(在功能內)以@@為前綴。 我認為這對於TASM來說是正常的,就像NASM .label本地標簽一樣。

   ; signed 32/16 => 32-bit division, using 32/16 => 16-bit division as the building block
   ; clobbers: CX, SI
IDIV32 PROC      ; DX:AX / BX = DX/AX rem BX
;global IDIV32_16       ; for testing with NASM under Linux
;IDIV32_16:
    ; 99 / 5   =  19 rem 4
    ; 99 / -5  = -19 rem 4
    ; -99 / 5  = -19 rem -4
    ; -99 / -5 =  19 rem -4

    mov   cx, dx          ; save high half before destroying it

;;; Check for simple case
    cwd                   ; sign-extend AX into DX:AX
    cmp   cx, dx          ; was it already correctly sign-extended?
    jne   @@dividend_32bit

    ; BUG: bx=-1  AX=0x8000 overflows with #DE
    ; also, this check rejects larger dividends with larger divisors
    idiv  bx
    mov   bx, dx
    cwd
    ret

;;; Full slow case: divide CX:AX by BX
   @@dividend_32bit:
    mov   si, ax                ; save low half
    mov   ax, cx                ; high half to AX for first div

                                ; CH holds dividend sign
    mov   cl, bh                ; CL holds divisor sign

 ;;; absolute value of inputs
    ; dividend in  AX:SI
    cwd                         ; 0 or -1
    xor   si, dx                ; flip all the bits (or not)
    xor   ax, dx
    sub   si, dx                ; 2's complement identity: -x = ~x - (-1)
    sbb   ax, dx                ; AX:SI = abs(dividend)

    test  bx, bx          ; abs(divisor)
    jnl  @@abs_divisor
    neg   bx
   @@abs_divisor:

 ;;; Unsigned division of absolute values
    xor   dx, dx
    div   bx             ; high half / divisor
    ; dx = remainder = high half for next division
    xchg  ax, si
    div   bx
 ;;; absolute result: rem=DX  quot=SI:AX
    mov   bx, dx
    mov   dx, si


 ;;; Then apply signs to the unsigned results
    test  cx,cx          ; remainder gets sign of dividend
    jns  @@remainder_nonnegative
    neg   bx
  @@remainder_nonnegative:

    xor   cl, ch         ; quotient is negative if opposite signs
    jns  @@quotient_nonnegative
    neg   dx
    neg   ax             ; subtract DX:AX from 0
    sbb   dx, 0          ; with carry
  @@quotient_nonnegative:

    ret
IDIV32 ENDP

優化:

  • 更簡單的符號保存和符號測試,使用x86的內置Sign Flag,從結果的MSB設置,如果SF == 1則跳轉js 避免將符號位向下移動到8位寄存器的底部。 可以使用xor / jns對相同符號進行測試,因為相同的符號將“取消”並且SF = 0,無論它是-0還是兩者都是1。 (一般來說,XOR可以用於比較相等,但它通常只對那些關心一位但不關心其他的按位情況有用)。

  • 避免單獨編寫CH,以便為現有的Intel CPU進行部分寄存器重命名。 此函數永遠不會將CH重命名為與ECX的其余部分分開。 (在像286這樣的老式CPU上, mov cx,dxmov ch,dh沒有任何缺點)。 我們還避免讀取高8位部分寄存器(例如test cx,cx而不是test ch,ch ),因為它在最近的英特爾Sandybridge系列CPU上具有更高的延遲。 Haswell / Skylake上的部分寄存器究竟是如何執行的?寫入AL似乎對RAX具有錯誤的依賴性,而AH是不一致的 )。 在P6系列中,寫入低8位部分寄存器會將它們與完整寄存器分開重命名,因此最好在寫入之后讀取8位部分寄存器。

    當然,在現代CPU上,像cx這樣的16位寄存器部分寄存器,即使在16位模式下也是如此(因為那里有32位寄存器),所以即使是mov cx,dx也依賴於ECX的舊值。


在386+

顯然,在32位寄存器/操作數大小可用的386+上 ,即使在16位模式下也可以使用它:

;; i386 version
    ;; inputs: DX:AX / BX
    shl   edx, 16
    mov   dx, ax         ; pack DX:AX into EDX
    mov   eax, edx

    movsx ebx, bx        ; sign-extend the inputs to 32 bit EBX
    cdq                  ; and 64-bit EDX:EAX
    idiv  ebx
    ; results: quotient in EAX, remainder in EDX

    mov   ebx, edx       ; remainder -> bx
    mov   edx, eax
    sar   edx, 16        ; extract high half of quotient to DX
    ;; result: quotient= DX:AX, remainder = BX

這可以#DE從BX = 0,或從DX溢出:AX = -2 ^ 31和BX = -1( LONG_MIN/-1


測試工具:

NASM包裝器從32位模式調用

%if __BITS__ = 32
global IDIV32
IDIV32:
    push   esi
    push   ebx
    push   edi      ; not actually clobbered in this version
    movzx  eax, word [esp+4  + 12]
    movzx  edx, word [esp+6  + 12]
    movzx  ebx, word [esp+8  + 12]
    call   IDIV32_16

    shl    edx, 16
    mov    dx, ax
    mov    eax, edx

    movsx  edx, bx       ; pack outputs into EDX:EAX "int64_t"

    pop    edi
    pop    ebx
    pop    esi
    ret
%endif

C程序,編譯為32位並與asm鏈接:

#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <limits.h>

// returns quotient in the low half, remainder in the high half (sign extended)
int64_t IDIV32(int32_t dxax, int16_t bx);

static int test(int a, short b) {
//  printf("\ntest %d / %d\n", a, b);
    int64_t result = IDIV32(a,b);

    int testrem = result>>32;
    int testquot = result;

    if (b==0 || (a==INT_MIN && b==-1)) {
        printf("successfully called with inputs which overflow in C\n"
               "%d/%d gave us %d rem %d\n",
               a,b, testquot, testrem);
        return 1;
    }
    int goodquot = a/b, goodrem = a%b;

    if (goodquot != testquot || goodrem != testrem) {
        printf("%d/%d = %d rem %d\t but we got %d rem %d\n",
               a,b, goodquot, goodrem, testquot, testrem);
        printf("%08x/%04hx = %08x rem %04hx\t but we got %08x rem %04hx\n"
               "%s quotient, %s remainder\n",
               a,b, goodquot, goodrem, testquot, testrem,
               goodquot == testquot ? "good" : "bad",
               goodrem == testrem ? "good" : "bad");
        return 0;
    }
    return 1;
}

int main(int argc, char*argv[])
{
    int a=1234, b=1;
    if(argc>=2) a = strtoll(argv[1], NULL, 0);  // 0x80000000 becomes INT_MIN instead of saturating to INT_MAX in 32-bit conversion
    if(argc>=3) b = strtoll(argv[2], NULL, 0);
    test(a, b);
    test(a, -b);
    test(-a, b);
    test(-a, -b);

    if(argc>=4) {
        int step=strtoll(argv[3], NULL, 0);
        while ( (a+=step) >= 0x7ffe) {  // don't loop through the single-idiv fast path
            // printf("testing %d / %d\n", a,b);
            test(a, b);
            test(-a, -b);
            test(a, -b);
            test(-a, b);
        }
        return 0;
    }
}

(這在intint32_t之間很草率,因為我只關心它在x86 Linux上運行,那里的類型相同。)

編譯

 nasm -felf32 div32-16.asm &&
 gcc -g -m32 -Wall -O3 -march=native -fno-pie -no-pie div32-test.c div32-16.o

使用./a.out 131076 -2 -1運行以使用divisor = -2測試從該值到0x7ffe(step = -1)的所有股息。 (對於-a / -ba / -b等的所有組合)

我沒有為商和除數做嵌套循環; 你可以用shell做到這一點。 我也沒有做任何聰明的事情來測試最大值附近的一些股息和一些接近底部的股息。

我重寫了我的idiv32程序,以便它否定一個負的被除數或除數為正/無符號形式,執行無符號除法,然后如果被除數XOR除數為真則否定商。

編輯:使用jsjns而不是測試80h的位掩碼。 不要再費力了。 剩余部分應該分享股息的標志,但由於我並不真正需要余數,所以我不打算使程序更加復雜以正確處理它。

idiv32 proc

;Divides a signed 32-bit value by a signed 16-bit value.

;alters ax
;alters bx
;alters dx

;expects the signed 32-bit dividend in dx:ax
;expects the signed 16-bit divisor in bx

;returns the signed 32-bit quotient in dx:ax

push cx
push di
push si

    mov ch, dh      ;ch <- sign of dividend
    xor ch, bh      ;ch <- resulting sign of dividend/divisor

    test dh, dh     ;Is sign bit of dividend set?  
    jns chk_divisor ;If not, check the divisors sign.
    xor di, di      ;If so, negate dividend.  
    xor si, si      ;clear di and si   
    sub di, ax      ;subtract low word from 0, cf set if underflow occurs
    sbb si, dx      ;subtract hi word + cf from 0, di:si <- negated dividend
    mov ax, di      
    mov dx, si      ;copy the now negated dividend into dx:ax

chk_divisor:
    xor di, di
    sbb di, bx      ;di <- negated divisor by default
    test bh, bh     ;Is sign bit of divisor set?
    jns uint_div    ;If not, bx is already unsigned. Begin unsigned division.
    mov bx, di      ;If so, copy negated divisor into bx.

uint_div:
    mov di, ax      ;di <- copy of LSW of given dividend
    mov ax, dx      ;ax <- MSW of given dividend
    xor dx, dx      ;dx:ax <- 0:MSW  
    div bx          ;ax:dx <- ax=MSW of final quotient, dx=remainder

    mov si, ax      ;si <- MSW of final quotient
    mov ax, di      ;dx:ax <- dx=previous remainder, ax=LSW of given dividend
    div bx          ;ax:dx <- ax=LSW of final quotient, dx=final remainder
    mov dx, si      ;dx:ax <- final quotient

    test ch, ch     ;Should the quotient be negative?
    js neg_quot     ;If so, negate the quotient.
pop si              ;If not, return. 
pop di
pop cx
    ret

neg_quot:
    xor di, di      
    xor si, si
    sub di, ax
    sbb si, dx
    mov ax, di
    mov dx, si      ;quotient negated
pop si
pop di
pop cx
    ret

idiv32 endp

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM