汇编中的定点乘法 (x86)

Fixed point multiplication in assembly (x86)

我想对一个无符号的 8.8 定点数进行乘除运算 ax 以 1.00125 注册并将结果存储在 ax 中。

我知道固定点 multiplication/division 需要一些额外的步骤 但我不知道如何在汇编中实现这些。

非常感谢帮助。

一个简单的解决方案是只使用 x87 浮点单元进行乘法运算。假设使用 nasm 的实模式(未测试):

example:
        push    bp
        mov     sp, bp         ; establish stack frame
        push    ax
        push    ax             ; make space for quotient
        fild    word [bp-2]    ; load number
        fld     st0            ; duplicate top of stack
        fmul    dword [factor] ; compute product
        fistp   word [bp-2]
        fmul    dword [invfac] ; compute quotient
        fistp   word [bp-4]
        pop     dx             ; quotient
        pop     ax             ; product
        pop     bp             ; tear down stack framt
        ret

factor  dd 1.00125
invfac  dd 0.999875        ; 1/1.00125

dx 中的商和 ax 中的积。舍入是根据 x87 FPU 中配置的舍入模式完成的(默认情况下应该舍入到最接近的舍入)。

关于定点乘法要理解的一点是结果的点是操作数 1 的点加上操作数 2 的点。

因此,当两个定点为零的数相乘时,我们得到定点为零的结果。

当两个数在 8 位定点(二进制)相乘时,我们得到一个在 16 位定点的数。

因此,需要根据需要缩小这样的结果。

如果您关心准确性,1.00125 不能以任何整数格式或任何浮点格式准确存储,因为它是二进制的递归分数(在二进制中它是 1.000000000101000111101011100001010001111010111...b 其中 00001010001111010111 序列永远重复)。因此,我将其转换为有理数 801/800;然后执行 x * 1.00125 = (x * 801) / 800(可能在除法上使用“舍入到最近”)。

如果您不关心准确性,那么“1.00125”可以使用的位数越多,结果就越接近正确答案。对于 8 位(“1.7 定点”),您可以获得的最接近值是 1.0000000b,这意味着您可以跳过乘法 (x * 1.00125 = x)。对于 16 位(“1.15 定点”),您可以获得的最接近值是 1.000000000101001b(或十进制的 1.001220703125)。

不过,你可以多作弊。具体来说,您可以通过 (x * 1) + (x * 0.00125) 在相同位数的情况下显着提高准确性。例如。您可以使用像 0.0000000001010001111010111b 这样的 16 位常量,而不是 1.000000000101001b(其中 9 位为零)这样的 16 位常量(其中 16 位是没有任何前导零的最后 16 位)。在这种情况下,常数非常接近(如 0.00124999880)而不是“不太接近”(如 1.001220703125)。

具有讽刺意味的是;只有 16 位,这个“0.00125”比 1.00125 的 32 位浮点表示更准确。

所以..在汇编中(假设一切都是未签名的)它可能看起来像:

    ;ax = x << 8 (or x as an 8.8 fixed point number)

    mov cx,ax         ;cx = x << 8

    mov bx,41943      ;bx = 41943 = 0.00124999880 << 25
    mul bx            ;dx:ax = (x << 8) * (0.00124999880 << 25) = x * 0.00124999880 << 33
                      ;dx = x * 0.00124999880 << 17
    shr dx,9          ;dx = x * 0.00124999880 << 17 >> 9 = x * 0.00124999880 << 8, carry flag = last bit shifted out
    adc dx,0          ;Round up to nearest (add 1 if last bit shifted out was set)

    lea ax,[dx+cx]    ;ax = x << 8 + x * 0.00124999880 << 8 = x * 1.00124999880 << 8

当然,这里的问题是将其转换回“8.8 定点”无论如何都会破坏大部分精度。为了保持大部分准确性,您可以改用 32 位结果(“8.24 定点”)。这可能看起来像:

    ;ax = x << 8 (or x as an 8.8 fixed point number)

    mov cx,ax         ;cx = x << 8

    mov bx,41943      ;bx = 41943 = 0.00124999880 << 25
    mul bx            ;dx:ax = (x << 8) * (0.00124999880 << 25) = x * 0.00124999880 << 33

    add ax,1 << 8     ;To cause the following shift to round to nearest
    adc dx,0

    shrd ax,dx,9
    shr dx,9          ;dx:ax = x * 0.00124999880 << 33 >> 0 = x * 0.00124999880 << 24

                      ;cx:0 = x << 24
    add dx,cx         ;dx:ax = x << 24 + x * 0.00124999880 << 24 = x * 1.00124999880 << 24

另一个问题是可能会溢出。例如。如果 x 是 0xFF.FF(或大约 255.996),结果将类似于 256.32,它太大而不适合“8.8”或“8.24”或“8.anything”定点格式.为避免该问题,您只需增加整数位数(并将精度降低 1 位) - 例如将结果设为“9.7 定点”或“9.23 定点”。

这里的重点是:

a) 对于“定点”计算,每个运算(乘法、除法、加法……)都会导致小数点移动。

b) 因为小数点是不断移动的,所以每一步小数点的位置最好采用标准的表示法。我的方法是在注释中包含一个“明确的转变”(例如“x << 8”而不仅仅是“x”)。 这种“注释中记录的显式移位”可以很容易地确定小数点移动的位置,并且 if/how 您需要移位 left/right 以转换为不同的定点格式。

c) 好的代码,需要注意准确性和溢出,这会导致小数点移动更多(并且使用“小数点位置的标准符号”更多重要)。