我将为那些可能面临需要在 FPU 上完成的计算的 x87 新手回答这个问题。
有两件事需要考虑。如果给你一个计算(INFIX notation),比如:
root := (2.0*root + x/(root*root)) / 3.0
有没有办法将其转换为 x87 FPU 可以使用的基本指令?是的,在非常基本的层面上,x87 FPU 是一个堆栈,其作用类似于复杂的RPN 计算器。您的代码中的等式是 INFIX 表示法。如果将其转换为 POSTFIX(RPN) 表示法,它可以很容易地实现为带有操作的堆栈。
document 提供了一些关于转换为 POSTFIX 表示法的信息。遵循您的 POSTFIX 等效规则如下所示:
2.0 root * x root root * / + 3.0 /
您可以使用 root=1 和 x=27 的这些键将其放入像 HP 15C 这样的旧 RPN 计算器 (HP):
2.0 [enter] root * x [enter] root [enter] root * / + 3.0 /
在线 HP 15C 应显示计算结果为 9.667。将其转换为基本的 x87:
- 数字是压栈顶 (fld)
- 变量是压入栈顶 (fld)
-
* 是 fmulp(ST(1) 乘以 ST(0),结果存入 ST(1),然后弹出寄存器栈)
-
/是fdivp(ST(1)除以ST(0),结果存入ST(1),弹出寄存器栈)
-
+是faddp(将ST(0)加到ST(1),结果存入ST(1),弹出寄存器栈)
-
- 为 fsubp(ST(1) 减去 ST(0),结果存入 ST(1),弹出寄存器栈)
您可以直接将2.0 root * x root root * / + 3.0 / 转换为 x87 指令:
fld Two ; st(0)=2.0
fld root ; st(0)=root, st(1)=2.0
fmulp ; st(0)=(2.0 * root)
fld xx ; st(0)=x, st(1)=(2.0 * root)
fld root ; st(0)=root, st(1)=x, st(2)=(2.0 * root)
fld root ; st(0)=root, st(1)=root, st(2)=x, st(3)=(2.0 * root)
fmulp ; st(0)=(root * root), st(1)=x, st(2)=(2.0 * root)
fdivp ; st(0)=(x / (root * root)), st(1)=(2.0 * root)
faddp ; st(0)=(2.0 * root) + (x / (root * root))
fld Three ; st(0)=3.0, st(1)=(2.0 * root) + (x / (root * root))
fdivp ; st(0)=((2.0 * root) + (x / (root * root))) / 3.0
掌握了基础知识后,就可以继续提高效率了。
关于编辑 2 / 后续问题
要记住的一件事是,如果您不使用将值从堆栈中弹出的指令,则循环的每次迭代都会消耗更多的 FPU 堆栈槽。通常以 P 结尾的 FPU 指令将值从堆栈中弹出。您不使用任何指令将项目从堆栈中移除,FPU 堆栈会不断增长。
与用户空间中的程序堆栈不同,FPU 堆栈非常有限,因为它只有 8 个插槽。如果您将超过 8 个活动值放入堆栈,您将收到 1#IND 形式的溢出错误。如果我们分析您的代码并在每条指令之后查看堆栈,我们会发现:
fld root ; st(0)=root
repreatAgain:
fmul two ; st(0)=(2.0*root)
fld root ; st(0)=root, st(1)=(2.0*root)
fmul st(0), st(0) ; st(0)=(root*root), st(1)=(2.0*root)
fld xx ; st(0)=x, st(1)=(root*root), st(2)=(2.0*root)
fdiv st(0), st(1) ; st(0)=(x/(root*root)), st(1)=(root*root), st(2)=(2.0*root)
fadd st(0), st(2) ; st(0)=((2.0*root) + x/(root*root)), st(1)=(root*root), st(2)=(2.0*root)
fld three ; st(0)=3.0, st(1)=((2.0*root) + x/(root*root)), st(2)=(root*root), st(3)=(2.0*root)
fld st(1) ; st(0)=((2.0*root) + x/(root*root)), st(1)=3.0, st(2)=((2.0*root) + x/(root*root)), st(3)=(root*root), st(4)=(2.0*root)
fdiv st(0), st(1) ; st(0)=(((2.0*root) + x/(root*root))/3.0), st(1)=3.0, st(2)=((2.0*root) + x/(root*root)), st(3)=(root*root), st(4)=(2.0*root)
jmp repreatAgain
观察到在最后一个 FDIV 指令之后和 JMP 之前,我们在堆栈上有 5 个项目(st(0) 到 st(4))。当我们进入循环时,我们只有 1 个,即 st(0) 中的 root。解决此问题的最佳方法是使用指令,使值随着计算的进行从堆栈中弹出(删除)。
另一种效率较低的方法是在重复循环之前释放堆栈中不再需要的值。 FFREE 指令可用于此目的,方法是从堆栈底部开始手动标记未使用的条目。如果您在上面的代码之后和jmp repreatAgain 之前添加这些行,代码应该可以工作:
ffree st(4) ; st(0)=(((2.0*root) + x/(root*root))/3.0), st(1)=3.0, st(2)=((2.0*root) + x/(root*root)), st(3)=(root*root)
ffree st(3) ; st(0)=(((2.0*root) + x/(root*root))/3.0), st(1)=3.0, st(2)=((2.0*root) + x/(root*root))
ffree st(2) ; st(0)=(((2.0*root) + x/(root*root))/3.0), st(1)=3.0
ffree st(1) ; st(0)=(((2.0*root) + x/(root*root))/3.0)
fst root ; Update root variable
jmp repreatAgain
通过使用 FFREE 指令,我们仅在 st(0) 中以新的root 结束循环。
由于您的计算方式,我还添加了fst root。您的计算包括 fld root,它依赖于每个循环完成时更新的 root 中的值。有一种更有效的方法可以做到这一点,但我提供的修复程序可以在您当前的代码中正常工作,而无需太多返工。
如果您使用我之前提供的低效/简单代码 sn-p 进行计算,您最终会得到如下代码:
finit ; initialize FPU
repreatAgain:
fld Two ; st(0)=2.0
fld root ; st(0)=root, st(1)=2.0
fmulp ; st(0)=(2.0 * root)
fld xx ; st(0)=x, st(1)=(2.0 * root)
fld root ; st(0)=root, st(1)=x, st(2)=(2.0 * root)
fld root ; st(0)=root, st(1)=root, st(2)=x, st(3)=(2.0 * root)
fmulp ; st(0)=(root * root), st(1)=x, st(2)=(2.0 * root)
fdivp ; st(0)=(x / (root * root)), st(1)=(2.0 * root)
faddp ; st(0)=(2.0 * root) + (x / (root * root))
fld Three ; st(0)=3.0, st(1)=(2.0 * root) + (x / (root * root))
fdivp ; newroot = st(0)=((2.0 * root) + (x / (root * root))) / 3.0
fstp root ; Store result at top of stack into root and pop value
; at this point the stack is clear again since
; all items pushed have been popped.
jmp repreatAgain
此代码不需要 FFREE,因为随着计算的进行,元素会从堆栈中弹出。 FADDP、FSUBP、FDIVP、FADDP 指令还会将值从栈顶弹出。这样做的副作用是使堆栈不参与部分中间计算。
集成循环
要将循环集成到我之前创建的简单/低效代码中,您可以使用FCOM (Floating point compare) 的变体进行比较。然后将浮点比较的结果传输/转换为常规 CPU 标志 (EFLAGS)。然后可以使用常规比较运算符来执行条件检查。代码可能如下所示:
epsilon REAL4 0.001
.CODE
main PROC
finit ; initialize FPU
repeatAgain:
fld Two ; st(0)=2.0
fld root ; st(0)=root, st(1)=2.0
fmulp ; st(0)=(2.0 * root)
fld xx ; st(0)=x, st(1)=(2.0 * root)
fld root ; st(0)=root, st(1)=x, st(2)=(2.0 * root)
fld root ; st(0)=root, st(1)=root, st(2)=x, st(3)=(2.0 * root)
fmulp ; st(0)=(root * root), st(1)=x, st(2)=(2.0 * root)
fdivp ; st(0)=(x / (root * root)), st(1)=(2.0 * root)
faddp ; st(0)=(2.0 * root) + (x / (root * root))
fld Three ; st(0)=3.0, st(1)=(2.0 * root) + (x / (root * root))
fdivp ; newroot=st(0)=((2.0 * root) + (x / (root * root))) / 3.0
fld root ; st(0)=oldroot, st(1)=newroot
fsub st(0), st(1) ; st(0)=(oldroot-newroot), st(1)=newroot
fabs ; st(0)=(|oldroot-newroot|), st(1)=newroot
fld epsilon ; st(0)=0.001, st(1)=(|oldroot-newroot|), st(2)=newroot
fcompp ; Do compare&set x87 flags pop top two values off stack
; st(0)=newroot
fstsw ax ; Copy x87 Status Word containing the result to AX
fwait ; Insure previous instruction completed
sahf ; Transfer condition codes to the CPU's flags register
fstp root ; Store result (newroot) at top of stack into root
; and pop value. At this point the stack is clear
; again since all items pushed have been popped.
jbe repeatAgain ; If 0.001 <= (|oldroot-newroot|) repeat
mov eax, 0 ; exit
ret
main ENDP
END
注意:FCOMPP 的使用和手动将 x87 标志转换为 CPU 标志是由代码顶部的 .586 指令驱动的。我假设因为您没有指定 .686 或更高版本,所以像 FCOMI 这样的指令不可用。如果您使用的是.686 或更高版本,那么代码的底部可能如下所示:
fld root ; st(0)=oldroot, st(1)=newroot
fsub st(0), st(1) ; st(0)=(oldroot-newroot), st(1)=newroot
fabs ; st(0)=(|oldroot-newroot|), st(1)=newroot
fld epsilon ; st(0)=0.001, st(1)=(|oldroot-newroot|), st(2)=newroot
fcomip st(0),st(1) ; Do compare & set CPU flags, pop one value off stack
; st(0)=(|oldroot-newroot|), st(1)=newroot
fstp st(0) ; Pop temporary value off top of stack
; st(0)=newroot
fstp root ; Store result (newroot) at top of stack into root
; and pop value. At this point the stack is clear
; again since all items pushed have been popped.
jbe repeatAgain ; If 0.001 <= (|oldroot-newroot|) repeat
从中缀表示法创建 RPN/Postfix 的快速方法
如果学习将 Infix 表示法转换为 RPN/Postfix 似乎与我之前在我的问题中链接的文档相比有点令人生畏,那么会有一些缓解。有许多网站可以为您完成这项工作。一个这样的网站是MathBlog。只需输入您的方程式,单击转换,它应该会显示 RPN/Postfix 等效项。它仅限于 +-/*、括号和带 ^ 的指数。
优化
优化代码的一大关键是通过将每个循环之间保持不变的部分与可变的部分分开来优化公式。常数部分可以在循环开始之前计算出来。
你原来的方程式是这样的:
分离常量部分我们可以得出:
如果我们将常量替换为 twothirds = 2.0/3.0 和 xover3 = x/3 的标识符,那么我们最终会得到一个简化的等式,如下所示:
如果我们将其转换为 POSTFIX/RPN,我们会得到:
twothirds root * xover3 root root * / +
彼得在更好的循环体部分下的回答中利用了类似的优化。他将常量Twothirds 和Xover3 放在循环外的x87 FPU 堆栈上,并在循环内根据需要引用它们。这避免了每次循环都必须从内存中不必要地重新读取它们。
基于上述优化的更完整示例:
.586
.MODEL FLAT
.STACK 4096
.DATA
xx REAL4 27.0
root REAL4 1.0
Three REAL4 3.0
epsilon REAL4 0.001
Twothirds REAL4 0.6666666666666666
.CODE
main PROC
finit ; Initialize FPU
fld epsilon ; st(0)=epsilon
fld root ; st(0)=prevroot (Copy of root), st(1)=epsilon
fld TwoThirds ; st(0)=(2/3), st(1)=prevroot, st(2)=epsilon
fld xx ; st(0)=x, st(1)=(2/3), st(2)=prevroot, st(3)=epsilon
fdiv Three ; st(0)=(x/3), st(1)=(2/3), st(2)=prevroot, st(3)=epsilon
fld st(2) ; st(0)=root, st(1)=(x/3), st(2)=(2/3), st(3)=prevroot, st(4)=epsilon
repeatAgain:
; twothirds root * xover3 root root * / +
fld st(0) ; st(0)=root, st(1)=root, st(2)=(x/3), st(3)=(2/3), st(4)=prevroot, st(5)=epsilon
fmul st(0), st(3) ; st(0)=(2/3*root), st(1)=root, st(2)=(x/3), st(3)=(2/3), st(4)=prevroot, st(5)=epsilon
fxch ; st(0)=root, st(1)=(2/3*root), st(2)=(x/3), st(3)=(2/3), st(4)=prevroot, st(5)=epsilon
fmul st(0), st(0) ; st(0)=(root*root), st(1)=(2/3*root), st(2)=(x/3), st(3)=(2/3), st(4)=prevroot, st(5)=epsilon
fdivr st(0), st(2) ; st(0)=((x/3)/(root*root)), st(1)=(2/3*root), st(2)=(x/3), st(3)=(2/3), st(4)=prevroot, st(5)=epsilon
faddp ; st(0)=((2/3*root)+(x/3)/(root*root)), st(1)=(x/3), st(2)=(2/3), st(3)=prevroot, st(4)=epsilon
fxch st(3) ; st(0)=prevroot, st(1)=(x/3), st(2)=(2/3), newroot=st(3)=((2/3*root)+(x/3)/(root*root)), st(4)=epsilon
fsub st(0), st(3) ; st(0)=(prevroot-newroot), st(1)=(x/3), st(2)=(2/3), st(3)=newroot, st(4)=epsilon
fabs ; st(0)=(|prevroot-newroot|), st(1)=(x/3), st(2)=(2/3), st(3)=newroot, st(4)=epsilon
fld st(4) ; st(0)=0.001, st(1)=(|prevroot-newroot|), st(2)=(x/3), st(3)=(2/3), st(4)=newroot, st(5)=epsilon
fcompp ; Do compare&set x87 flags pop top two values off stack
; st(0)=(x/3), st(1)=(2/3), st(2)=newroot, st(3)=epsilon
fstsw ax ; Copy x87 Status Word containing the result to AX
fwait ; Insure previous instruction completed
sahf ; Transfer condition codes to the CPU's flags register
fld st(2) ; st(0)=newroot, st(1)=(x/3), st(2)=(2/3), st(3)=newroot, st(4)=epsilon
jbe repeatAgain ; If 0.001 <= (|oldroot-newroot|) repeat
; Remove temporary values on stack, cubed root in st(0)
ffree st(4) ; st(0)=newroot, st(1)=(x/3), st(2)=(2/3), st(3)=epsilon
ffree st(3) ; st(0)=newroot, st(1)=(x/3), st(2)=(2/3)
ffree st(2) ; st(0)=newroot, st(1)=(x/3)
ffree st(1) ; st(0)=newroot
mov eax, 0 ; exit
ret
main ENDP
END
此代码在进入循环之前将这些值放在堆栈中:
- st(4) =
Epsilon 值 (0.001)
- st(3) = 计算完成前
root 的副本(实际上是prevroot)
- st(2) = 常数
Twothirds (2/3)
- st(1) =
Xover3 (x/3)
- st(0) =
root 的活动副本
在循环重复之前,堆栈将具有上面的布局。
退出前最后的代码会删除所有临时值,并在 st(0) 中将堆栈的值 root 留在顶部。