我正在尝试使用8086处理器编写汇编程序,该处理器将找到数字的立方根。显然我使用的是浮点数。
基于Newton-Raphson method的算法:
root := 1.0;
repeat
oldRoot := root;
root := (2.0*root + x/(root*root)) / 3.0
until ( |root – oldRoot| < 0.001;
如何将(2 * root + x)除以(root * root)?
.586
.MODEL FLAT
.STACK 4096
.DATA
root REAL4 1.0
oldRoot REAL4 2.0
Two REAL4 2.0
inttwo DWORD 2
itThree DWORD 3
three REAL4 3.0
x DOWRD 27
.CODE
main PROC
finit ; initialize FPU
fld root ; root in ST
fmul two ; root*two
fadd x ; root*two+27
fld root ; root in ST
fimul root ; root*root
mov eax, 0 ; exit
ret
main ENDP
END
我想我不明白堆栈中的什么位置。该产品是否为行
fimul root;根*根
进入ST(1)?编辑不,它进入st(0)st(0)中的内容被压入堆栈到st(1)
但是我还没有想出我的问题的答案...... 我如何划分?现在我看到我需要将st(1)除以st(0)但是我不知道怎么做。我试过了。
finit ; initialize FPU
fld root ; root in ST
fmul two ; root*two
fadd xx ; root*two+27
; the answer to root*two+x is stored in ST(0) when we load root st(0) moves to ST1 and we will use ST0 for the next operation
fld root ; root in ST previous content is now in ST1
fimul root ; root*root
fidiv st(1)
编辑: 我的公式写错了。这就是我要找的。 p>
(2.0*root) + x / (root*root)) / 3.0 That's what I need.
STEP 1) (2 * root)
STEP 2) x / (root * root)
STEP 3) ADD step one and step 2
STEP 4) divide step 3 by 3.0
root =(2.0 * 1.0)+ 27 /(1.0 * 1.0)/ 3.0; (2)+ 27 /(1.0)/ 3.0 = 11 ==&gt; root = 11
EDIT2:新代码!!
.586
.MODEL FLAT
.STACK 4096
.DATA
root REAL4 1.0
oldRoot REAL4 2.0
Two REAL4 2.0
three REAL4 3.0
xx REAL4 27.0
.CODE
main PROC
finit ; initialize FPU
fld root ; root in ST ; Pass 1 ST(0) has 1.0
repreatAgain:
;fld st(2)
fmul two ; root*two ; Pass 1 ST(0) has 2 Pass 2 ST(0) = 19.333333 st(1) = 3.0 st(2) = 29.0 st(3) = 1.0
; the answer to roor*two is stored in ST0 when we load rootSTO moves to ST1 and we will use ST0 for the next operation
fld root ; root in ST(0) previous content is now in ST(1) Pass 1 ST(0) has 1.0 ST(1) has 2.0 Pass 2 st(
fmul st(0), st(0) ; root*root ; Pass 1 st(0) has 1.0 st(1) has 2.0
fld xx ; Pass 1 st(0) has 27.0 st(1) has 1.0 st(2) has 2.0
fdiv st(0), st(1) ; x / (root*root) ; Pass 1: 27 / 1 Pass 1 st(0) has 27.0 st(1) has 2.0 st(2) has 2.0
fadd st(0), st(2) ; (2.0*root) + x / (root*root)) Pass 1 st(0) has 29.0 st(1) has 1.0 st(2) has 2.0
fld three ; Pass 1 st(0) has 3.0 st(1) has 29.0 st(2) has 1.0 st(3) has 2.0
fld st(1) ; Pass 1 st(0) has 3.0 st(1) has 29.0 st(2) = 1.0 st(3) = 2.0
fdiv st(0), st(1) ; (2.0*root) + x / (root*root)) / 3.0 Pass 1 st(1) has 9.6666666666667
jmp repreatAgain
mov eax, 0 ; exit
ret
main ENDP
END
答案 0 :(得分:5)
英特尔insn参考手册记录了所有说明,包括fdiv
和fdivr
(x / y而不是y / x)。如果你真的需要学习大部分过时的x87(fdiv
)而不是SSE2(divss
),那么this x87 tutorial is essential reading,尤其是。解释寄存器堆栈的早期章节。另请参阅this x87 FP comparison Q&A。请参阅x86代码wiki中的更多链接。
re:EDIT2代码转储:
循环中有4条fld
条指令,但没有p
- 后缀操作。您的循环将在第3次迭代时溢出8寄存器FP堆栈,此时您将获得NaN。 (具体来说,不定值NaN,printf打印为1#IND
。
我建议设计您的循环,以便迭代从root
中的st(0)
开始,并以root
中的下一个迭代st(0)
值结束{1}} 即可。不要在循环内加载或存储到root
。使用fld1
在循环外加载1.0作为初始值,在循环后加载fstp [root]
以将st(0)
弹出到内存中。
你选择了最不方便的方法来做tmp / 3.0
; stack = tmp (and should otherwise be empty once you fix the rest of your code)
fld three ; stack = 3.0, tmp
fld st(1) ; stack = tmp, 3.0, tmp ; should have used fxchg to just swap instead of making the stack deeper
fdiv st(0), st(1) ; stack = tmp/3.0, 3.0, tmp
fdiv
,fsub
等具有多个注册表单:一个st(0)
是目标,另一个是源。以st(0)
作为来源的表单也可以p
操作,因此您可以
fld three ; stack = 3.0, tmp
fdivp ; stack = tmp / 3.0 popping the stack back to just one entry
; fdivp st(1), st(0) ; this is what fdivp with no operands means
如果直接使用内存操作数而不是加载它,它实际上甚至更简单。由于您需要st(0) /= 3.0
,,您可以执行fdiv [three]
。在这种情况下,FP ops就像整数运算一样,你可以div dword ptr [integer_from_memory]
使用内存源操作数。
非交换操作(减法和除法)也有反向版本(例如fdivr
),这可以为您节省fxchg
或让您使用内存操作数,即使您已经需要3.0 / tmp而不是tmp / 3.0
除以3与乘以1/3 相同,fmul
比fdiv
快得多。从代码简单的角度来看,乘法是可交换的,因此实现st(0) /= 3
的另一种方法是:
fld [one_third]
fmulp ; shorthand for fmulp st(1), st(0)
; or
fmul [one_third]
注意1/3.0
在二进制浮点中没有精确表示,但是+/-约2 ^ 23之间的所有整数都是(单精度REAL4的尾数的大小)。如果你期望使用精确的三倍数,你应该只关心这个。
您可以通过提前2.0 / 3.0
和x/3.0
将分组提升出来。如果您希望循环平均运行多次迭代,那么这是值得的。
您可以使用fld st(0)
复制堆栈顶部,这样您就不必继续从内存加载。
fimul [root]
(整数 mul)是一个错误:您的root
采用REAL4
(32位浮点)格式,而不是整数。 fidiv
同样是一个错误,当然也不能将x87寄存器用作源操作数。
由于您在堆栈顶部有root
,我认为您可以fmul st(0)
使用st(0)
作为显式和隐式操作数,从而生成st(0) = st(0) * st(0)
,堆栈深度没有变化。
你也可以使用sqrt作为比1.0更好的初始近似,或者+/-1 * sqrtf(fabsf(x))
。我没有看到x87指令将一个浮点数的符号应用到另一个浮点数,只是fchs
无条件地翻转,fabs
无条件地清除符号位。有fcmov
,但需要P6或更高版本的CPU。您提到了8086,但之后使用了.586
,因此IDK就是您要定位的内容。
未经过调试或测试,但您的代码充满了来自相同数据的重复加载让我发疯。这个优化版本在这里是因为我很好奇,不是因为我认为它会直接帮助OP。
此外,希望这是如何在代码中评论数据流的一个很好的例子。 (例如x87,或带有shuffle的矢量化代码)。
## x/3.0 in st(1)
## 2.0/3.0 in st(2)
# before each iteration: st(0) = root
# after each iteration: st(0) = root * 2.0/3.0 + (x/3.0 / (root*root)), with constants undisturbed
loop_body:
fld st(0) ; stack: root, root, 2/3, x/3
fmul st(0), st(0) ; stack: root^2, root, 2/3, x/3
fdivr st(0), st(3) ; stack: x/3 / root^2, root, 2/3, x/3
fxchg st(1) ; stack: root, x/3/root^2, 2/3, x/3
fmul st(0), st(2) ; stack: root*2/3, x/3/root^2, 2/3, x/3
faddp ; stack: root*2/3 + x/3/root^2, 2/3, x/3
; TODO: compare and loop back to loop_body
fstp [root] ; store and pop
fstp st(0) ; pop the two constants off the FP stack to empty it before returning
fstp st(0)
; finit is very slow, ~80cycles, don't use it if you don't have to.
32位函数调用约定将FP结果返回到st(0),因此您可以这样做,但调用者可能必须存储在某处。
答案 1 :(得分:4)
我将在非常基本的层面为那些可能面临需要在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
{/ 1}}中的这些密钥将其放入旧的RPN计算器(HP),例如HP 15C
2.0 [enter] root * x [enter] root [enter] root * / + 3.0 /
在线HP 15C应显示该计算结果为9.667。将其翻译为基本x87:
*
是fmulp(ST(0)乘以ST(1),将结果存储在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
一旦掌握了基础知识,就可以继续提高效率。
要记住的一件事是,如果您不使用从堆栈中弹出值的指令,则循环的每次迭代将消耗更多的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))。当我们进入循环时,我们在 st(0)中只有1 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
更新的值。有一种更有效的方法可以做到这一点,但是我提供了一个可以在当前代码中运行的修复,而不需要太多的工作。
如果您使用我之前提供的效率低下/简单的代码片段进行计算,您最终会得到以下代码:
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
如果学习将 Infix 符号转换为 RPN / Postfix ,那么我在之前在我的问题中链接的文档似乎有点令人生畏,有一些缓解。有很多网站会为您完成这项工作。其中一个网站是MathBlog。只需输入您的等式,单击转换,它应显示 RPN / Postfix 等效项。它仅限于+ - / *,括号和带有^。
的指数优化代码的一个关键是通过将每个循环之间保持不变的部分与可变部分分开来优化公式。可以在循环开始之前计算常量部分。
你原来的等式是:
分离我们可以得到的常数部分:
如果我们用twothirds
= 2.0 / 3.0和xover3
= x / 3的标识符替换常量,那么我们最终会得到一个如下所示的简化公式:
如果我们将其转换为 POSTFIX / RPN ,那么我们得到:
twothirds root * xover3 root root * / +
Peter在 Better loop body 部分的答案中正在利用类似的优化。他将常量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
此代码在进入循环之前将这些值放在堆栈上:
Epsilon
值(0.001)root
的副本(有效prevroot
)Twothirds
(2/3)Xover3
(x / 3)root
在循环重复之前,堆栈将具有上面的布局。
退出前结束时的代码将删除所有临时值,并在 st(0)的顶部将堆栈中的值root
放置。