MIPS:除法算法(除 IEEE-754 格式的有效数字)对最后 4-5 位 (LSB) 给出错误答案

MIPS: Division algorithm (Dividing significands of IEEE-754 format) gives incorrect answer for the last 4-5 bits (LSB)

对于除数>被除数的情况,计算出的尾数最后4-5个最低有效位给出了错误的结果。

我正在尝试除以两个 IEEE-754 浮点数的 significands/mantissa。我用过这个除法算法

当除数>被除数时,尾数归一化,但最后 4-5 位仍然不正确。

除法算法:#将除数尾数($a0)除以除数尾数($a1)进行 25 次迭代# #Returns $v0 中的商值#

    addi $sp, $sp, -12      #Decrement in $sp (Stack Pointer)
    sw $s0, 0($sp)      #Pushing $s0 into Stack
    sw $ra, 4($sp)      #Pushing $ra into Stack (Function Call Exists)
    sw $s1,8($sp)       #Pushing $s1 into stack 

    move $t0, $a0       #$t0 = Mantissa of Dividend/Remainder
    move $t1, $a1       #$t1 = Mantissa of Divisor

    add $s0, [=10=], [=10=]     #$s0 = Initialization
    add $v1, [=10=], [=10=]     #$v1 = 0 (Displacement of Decimal Point Initialized)
    addi $s1,[=10=],1       #$s1 = 1 (initialize loop variable to 1)
    add $t3,[=10=],33

loop:   
    beq $t1, [=10=], check      #If Divisor = 0, Branch to check
    sub $t0, $t0, $t1       #Dividend = Dividend - Divisor
    sll $s0, $s0, 1     #Quotient Register Shifted Left by 1-bit
    slt $t2, $t0, [=10=]
    bne $t2, [=10=], else       #If Dividend < 0, Branch to else
    addi $s0, $s0, 1        #Setting Quotient LSb to 1
    j out
else:   add $t0, $t0, $t1       #Restoring Dividend Original Value

out:    srl $t1, $t1, 1     #Divisor Register Shifted Right by 1-bit
    j loop

check:  slt $t2, $a0, $a1       #If Dividend < Divisor, Call Function 'Normalization'
    beq $t2, [=10=], exit       #If Dividend > Divisor, Branch to exit
    move $a0, $s0       #$a0 = Quotient

    jal Normalization       #Function Call 
    j return

exit:   move $v0, $s0       #$v0 = Calculated Mantissa

return: lw $ra, 4($sp)      #Restoring $ra
    lw $s0, 0($sp)      #Restoring $s0
    lw $s1, 8($sp)      #restoring $s1
    addi $sp, $sp, 8        #Increment in $sp (Stack Pointer)
    jr $ra          #Return

归一化:#归一化尾数(在$a0中)并计算小数点移动的小数位# #Returns: # i) $v0 = 归一化尾数 # # ii) $v1 = 小数位数#

    lui $t0, 0x40       #$t0 = 0x40 (1 at 23rd-bit)
    addi $t2, [=11=], 1     #$t2 = 1 (Initialization)

loop2:  and $t1, $a0, $t0       #Extracting 23rd-bit of Mantissa 
    bne $t1, [=11=], else2      #If 23rd-bit = 1; Branch to else2
    addi $t2, $t2, 1        #Increment in Count of Decimal Places Moved
    sll $a0, $a0, 1     #Mantissa Shifted Left (To Extract Next Bit)
    j loop2         

else2:  sll $a0, $a0, 1     #Setting 24th-bit = 1 (Implied)
    move $v0, $a0       #$v0 = Normalized Mantissa
    move $v1, $t2       #$v1 = Displacement of Decimal Point    
    jr $ra          #Return

比如我预计2.75/6.355的输出是00111110110111011000111011001110但实际输出是00111110110111011000111011010110.

你的算法不正确。

恢复 division 的正确算法是

qu=0
rem=dividend
repeat N times
  rem = rem - divisor
  if rem < 0
    rem = rem + divisor
    qu = (qu<<1)
  else
    qu = (qu<<1) + 1
  end
  rem = rem << 1
end

而你的是

qu=0
rem=dividend
repeat N times
  rem = rem - divisor
  if rem < 0
    rem = rem + divisor
    qu = (qu<<1)
  else
    qu = (qu<<1) + 1
  end
  divisor = divisor >> 1  // instead of left shift remainder
end

由于算法只依赖于divisorrem的比较,所以看起来相当于右移divisor或左移rem。但事实并非如此。
当右移 divisor 时,您丢失了 divisor 的最低有效位。
这可能会影响比较,从而影响商数。
如果你打印余数,你会发现它有很大的影响,它的值可能与正确结果相差 2 倍。

余数乘以2好像很危险,可能会溢出。但是如果我们看一下算法,我们可以看到它不会发生。
最初 dividend 和 divisor 是一些 FP 的尾数,因此 1 ≤ divisor,dividend < 2 并且同样适用于最初是 a 的 rem dividend 的副本。请注意,这意味着 rem<2*div
现在,我们进行第一个计算步骤
⚫ 如果 rem < div,那么我们将它乘以 2,并且 rem(=2*rem)<2*div.
⚫ 如果 rem ≥ div,那么我们计算 rem−div 并将其乘以 2.
由于最初 rem < 2*div,rem(=2*(rem− div))<2*(2*div− div),并且 属性 rem<2*div 仍然成立。

所以在每一步我们总是有 属性 rem<2*div 并且,假设 2*div 可以编码,这确保 rem 永远不会溢出。

在实现方面,您可以将这些数字编码在整数寄存器的 24 LSB 上。基本上已经足够了,因为精度保持不变。
在您的实现中,您循环了 32 次。如果想把结果写成IEEE尾数,那是没用的,可以减少迭代次数。循环
就足够了 24倍(尾数大小)
+ 1(如果 dividend < divisor,第一位将为 0,但第二位保证为 1)

如果要对结果进行舍入,则必须执行两个额外的步骤来获取舍入位和保护位。经过这两个计算步骤,如果余数≠0,则将粘性位设置为1,否则将其设置为0。然后按照通常的规则进行舍入。

我。以您开始的形式描述除法算法本身(据我所知,汇编代码也是如此)缺少主要部分。在准备 运行 除法循环时,当您在每次迭代中将除数右移 1 位时,您最初应该尽可能将其左移并且有用。

让我用十进制数解释一下:假设您在 6 位机器中将 10001 除以 73(所以这是 010001 除以 000073)。您应该将 73 向左移动,直到它停止以适合(因此最大移动为 4,移动的除数为 730000)或它的最高有效位 (MSD) 位置高于股息的 MSD(因此,我们可以停在 73000)。那么,循环就是运行和

  • shifted_divisor = 73000,移位 = 3
  • shifted_divisor = 7300,移位 = 2
  • shifted_divisor = 730,移位 = 1
  • shifted_divisor = 73,移位 = 0

(十进制时,每次移位都要嵌套循环减法;二进制则不需要。)

如果您将 73 右移更多,您将丢失除数中的有效数字,因此最终的商将大于精确的一个。

您可以无条件地将最大宽度左除数移位(730000,shift=4),但这会浪费CPU个周期。如果CPU有CountLeadingZeros操作,可以节省时间

二.第二个主要时刻是你正在划分从 floating-point 数字中得到的尾数。这意味着,在两个标准化数字的最典型情况下,被除数和除数的 MSB 将相同,您将只得到商的 2 个变体 - 0 和 1。(对于 IEEE binary32,这是设置了第 23 位并且股息和除数都在 8388608..16777215 范围内。)

要得到更多的实数,你必须进行长除法。 (请参阅下文以了解更经济的方法;现在,只需与教科书除法进行比较。)对于 IEEE binary32,这意味着在最后舍入之前至少获得 27 个商数(24 个结果尾数 + 守卫 + 舍入 + 粘性)。如上所述,将被除数左移 27 位,然后在 28 个周期内继续将 51 位被除数除以 24 位除数。

@AlainMerigot 描述的版本与我描述的基本相同 - 您仍然计算 (dividend<<quotient_width)/divisor,但方式不太明显。如果 left-shift 余数除以 1,这可能与 right-shift 除数除以 1 相同,前提是没有数据丢失。因此,第一次迭代(第 0 次)比较相等的位置并提供结果的 MSB,但此时它位于第 0 位;然后,您 "activate" 在每次迭代中多一点余数。这允许避免 double-word 操作。在这个算法中,如果你需要最后的余数,它应该通过按迭代次数右移来恢复;但是浮动除法本身不需要它。

但是:为了使其正常工作,您应该从一开始就正确对齐股息和除数。在十进制示例中,这意味着您不应该以 divident=10001, divisor=73 开始,而应该以 dividend=10001, divisor=73000 开始。如果在获取整数值之前对源尾数进行了归一化,则自动满足。否则,需要额外的努力,它们实际上与我描述的教科书除法的除数转换相同。 (看起来这就是为什么英特尔 CPUs 几十年来一直遭受无限非规范处理时间的困扰;))