8086/8087/8088 MACRO ASSEMBLY LANGUAGE REFERENCE MANUAL(c)1980 Intel Corporation提到(强调我的):
... 8087提供了一个非常好的近似真实的系统。然而,重要的是要记住,它不是一个精确的表示,真实的的算术本质上是近似的。
相反,同样重要的是,8087确实对实数的整数子集执行精确的算术。也就是说,对两个整数的运算返回精确的整数结果,前提是真结果是整数并且在范围内。
最近的一本手册甚至更简洁(强调他们的):
内务部的处理器它们可以处理最多18位的十进制数而不会出现舍入错误,对大到2^64(或10^18)的整数执行精确算术。
FPU支持的整数数据类型包括有符号字(16位)、有符号双字(32位)和有符号qword(64位)。从来没有提到过未签名。事实上,关于FPU的一切都是有符号的,甚至支持有符号的零(+0和-0)。
那么,有没有可能使用FPU来划分一对 * 无符号 * 64位数字,并获得精确的商和余数?
对于两个带符号的64位数字的除法,我编写了下一个代码。商看起来很好,但余数总是返回零。这是为什么呢?
; IN (edx:eax,ecx:ebx) OUT (edx:eax,ecx:ebx,CF)
FiDiv: push edi ecx ebx edx eax
mov edi, esp
fninit
fild qword [edi] ; Dividend
fild qword [edi+8] ; Divisor
fld
fnstcw [edi]
or word [edi], 0C00h ; Truncate Towards Zero
fldcw [edi]
fdivr st2 ; st0 = st2 / st0
fld ; Duplicate because `fistp` does pop
fistp qword [edi] ; Quotient
fmulp ; st1 *= st0, pop st0
fsubp ; st1 -= st0, pop st0
fistp qword [edi+8] ; Remainder
fnstsw ax
test ax, 101b ; #Z (Divide-By-Zero), #I (Invalid Arithmetic)
pop eax edx ebx ecx edi
jnz .NOK
ret ; CF=0
.NOK: stc ; Overflow on 8000000000000000h / -1
ret ; or Division by zero
; ------------------------------
FPU舍入模式设置为“Truncate Towards Zero”(即“Chop”),以模拟ALU idiv
指令的行为。
1条答案
按热度按时间zyfwsgd61#
零余数
此代码从以下公式计算余数:
由于舍入模式被设置为“Truncate Towards Zero”,因此
fistp qword [edi]
指令将在存储到内存之前将ST0中保存的商(的副本)转换为整数。然而,保留在(fpu)堆栈上的商的值仍然是带有分数的实数。一旦与除数相乘,它将再次产生被除数,结果是零余数。缺少的是将商四舍五入为整数,并在(fpu)堆栈上执行:
但更快的方法是简单地从内存中重新加载整数商:
无符号分割
在内部,FPU将64位专用于数字的有效位,另加一位用于数字的符号。FPU可以表示从-18 '446744073' 709551616到18 '446744073' 709551616范围内的每个整数。64位有效数允许我们处理范围从0到18 '446744073' 709551615的无符号64位整数。唯一的麻烦是如何加载和存储这些
fild
和fistp
无法处理的值(因为它们被限制在从-9'223372036 '854775808到9'223372036' 854775807的范围内操作)。可以在无符号四字和扩展实数格式之间来回转换,因此我们可以使用
fld
和fstp
。另一种方法将无符号四字从其上半部和下半部加载/存储到其上半部和下半部。但是转换需要时间,所以我发现,通过消除足够多的特殊情况,剩下的唯一麻烦的操作是被除数的加载。其他一切都可以像往常一样使用fild
和fistp
。特殊情况包括:
在需要实际的
fdiv
的情况下,代码首先加载被除数的一半,在(fpu)堆栈上将其加倍,如果真正的被除数是奇数,则有条件地添加1。更快的结果
引用一个使用名为Chunking的技术来除两个64位整数的答案:
即使你使用的是64位数据类型,实践告诉我,(通用)程序中的大多数除法仍然可以只使用内置的
div
指令。这就是为什么我在代码中添加了一个检测机制来检查ECX:EBX中的除数是否小于4GB(因此适合EBX),以及EDX中被除数的扩展是否小于EBX中的除数。如果满足这些条件,则正常的div
指令执行任务,并且执行速度也更快。由于某种原因(如学校)使用div
是不允许的,然后只需删除前缀代码即可清除。今天的代码可以从相同的前缀中受益,但事实证明,首先检测特殊情况更有益: