CPU中的FPU
sign+significand+exponent
一开始FPU和CPU是分开的,FWAIT用来转换CPU状态,等待FPU完成工作。FPU有一个栈,用来保存8个80比特的寄存器ST(0)……ST(7)。
ARM和MIPS不是栈,而是一些寄存器,在x86/x64的SIMD拓展也是。
2种浮点类型,float(32bits)和double(64bits)。
return and printf a/3.15+b*4.1
fld QWORD PTR _a$[ebp]
fdiv QWORD PTR _real@40091eb851eb851f 即a/3.14
ST(0)=a, 用a/3.14放入ST(0)
ST(0)=b,先把ST(0)->ST(1)因为是栈
ST(0)=b*4.1 -> ST(0)
把ST(0)、ST(1)加起来->ST(0)
注意FPU的这个栈是循环的。
不一样的是,第一步ST(0)=3.14
fdivv [ebp+arg_0], 其中arg_0存放a
乘法也是,乘数与被乘数顺序与x86不同。
VFP标准,没有栈,只有寄存器,D开头双精度,S开头单精度。
Thumb-2的代码是一样的。
调用了一些库函数,模拟FPU,但其实是软件实现的。经济。
ldr d2, .LC25 ;3.14
fdiv d0, d0, d2 ;计算除法
ldr d2, .LC26 ;4.1
fwadd d0, d1, d2, d0 ;计算乘法和加法
ret
没有必要地把值倒来倒去。
最多可以支持4个coprocessor。也没有栈,用寄存器。
LWC1加载32位字到第一个coprocessor的寄存器。
DIV.D MUL.D ADD.D
一个简单的例子:
printf(“32.01 ^ 1.54 = %lf\n”, pow(32.01, 1.54));
先给第一个变量分配空间,然后使用fld和fstp指令。这两个指令把变量在数据段和FPU栈之间进行移动。所以这两条语句实现了将32.01从数据段移到栈中。同样地,1.54也被移到栈中,然后调用了pow函数,函数返回值存放在ST(0)中。然后用fstp从ST(0)移动到本地栈,调用printf函数。
64位的浮点数是用寄存器传递的,而不是栈。未优化的代码有点冗余。pow函数接收R0+R1为第一个参数,R2+R3为第二个参数。结果保存在R0+R1。pow函数的结果随后会被移动到D16中,然后再存到R1+R2里,printf就从这两个寄存器接受参数。
还是用R0+R1存第一个参数,R2+R3存第二个参数。然后直接把pow函数结果放到了R3+R2,然后调用printf,不过这里没有使用D开头的寄存器,只使用了R开头的寄存器。
常数加载到D0和D1,这是pow的参数。结果放在D0中。然后会把D0直接传递给printf。实际上printf一般从X寄存器取整数类型值,从D寄存器取浮点数类型值。
LUI指令将浮点数的32位放到 V0,但是这一步是多余的,不知道为什么要加这一步。新的指令是MFC1,意思是从多处理器1移出。FPU的号码就是多处理器1。这个指令从处理器的寄存器中移出数据到CPU寄存器。pow的结果会被放到 A3和$A2。printf就从这两个寄存器读取参数。
比较两个浮点数的大小。
比较的时候,b放在ST(0)中,a放在栈上。首先FLD将b加载到ST(0)中。FCOMP比较ST(0)和a的值,然后设置FPU状态字寄存器中的C3/C2/C0。这是一个16位的寄存器,保存FPU的状态。设置好位之后,FCOMP从栈上弹出变量,这就是与FCOM的不同点。不过这是老CPU的做法,现代CPU使用FCOMI/FCOMIP/FUCOMI/FUCOMIP指令,做同样的事,但修改的是ZF/PF/CF的flags。
FNSTSW指令将FPU的状态字寄存器中的内容复制给AX。C3/C2/C0放在14/10/8位置,与AX寄存器相同位置,都放在高地址部分AX-AH。
如果b>a,C3/C2/C0设置为0,0,0
a>b,0,0,1
a=b,1,0,0
结果出错,则是1,1,1
下面的指令test ah, 5,只考虑C0和C2位,其他的都被忽略了。然后会根据AX的值设置PF的值,根据PF值判断是否跳转。
如果条件跳转触发,FLD会将b的值加载到ST(0)。如果没有触发,就加载a的值。
注意C2主要是存放错误信息的位。
FCOM与FCOMP不一样,没有POP这一步,不更改FPU栈。与上一个例子不同,这里操作数是反的顺序。
如果b>a,C3/C2/C0设置为0,0,0
a>b,0,0,1
a=b,1,0,0
test ah, 65检查2个位,即C3和C0。然后FSTP ST(1)把值从ST(0)复制到ST(1),如果a>b就是a。
注意这个比较的两个数放在ST(0)和ST(1)中。
FSTP ST相当于pop这个FPU栈。
FUCOMPP与FCOM差不多,从栈中弹出两个变量。FPU可以处理非数字的值,如NaN。FCOM会触发异常,如果操作数是NaN。
SAHF将AH值存储到flags中。
fnstsw ax/sahf指令将C3/C2/C0移动到ZF, PF, CF。其实具体的操作与上面的都差不多。
如果a>b,CPU设置为ZF=0, PF=0, CF=0。
a
与上面的差不多,除了在SAHF之后使用JA。JA是在CF和ZF都为0的情况下触发,其实与上面的结果一样,但是节省了大量的代码。
FUCOMI比较ST(0)中的a值和ST(1)中的b值,然后在CPU设置一些FLAGS。FCMOVBE检查flags并将ST(1)的b值复制给ST(0),如果a
输入值a和b分别放在D16和D17中。然后使用VCMPE指令进行比较。ARM有自己的flag寄存器,即FPSCR。VMRS将4位(N, Z, C, V)从处理器的状态字拷贝到通用状态寄存器APSR。
VMOVGT是D寄存器的指令,如果比较两个操作数,前者大于后者就执行(GT)。如果执行,a的值写入的D16(原本存在D17)。否则就是b在D16中。
VMOV准备好D16中的值,通过R0和R1返回值。
与之前的例子基本相同,但有一点不一样。我们已经知道ARM模式的很多指令可以通过条件谓词支持,但是Thumb模式中没有。IT GT指令表示if-then条件。ITE是if-then-else。
基本相同,但是有很多冗余代码,因为a和b变量存在局部栈上。
Keil不生成FPU指令,调用外部库来完成比较:_aeabi_cdrcmple。
有FPU指令。首先是FCMPE,比较D0和D1中的值,然后设置APSR标签(N, Z, C, V)。FCSEL根据条件不同将D0或D1的值赋值给D0。条件比较要根据APSR。
把参数存放在本地栈。然后从本地栈放到X0/X1,然后再放到D0/D1进行比较。本质上是使用了传统的BLE进行跳转,借用寄存器X0。最后把X0返回到D0进行printf的调用。
试着手动优化上面的代码。
把double换成float。这时没有用D寄存器,而是用了S寄存器。
在FPU里设置条件,在CPU中检查。检查位放在FCCR中。C.LT.D比较两个值。LT是条件小于的意思。D表示数据类型为double。依据比较结果,FCC0是条件位被设置。
BC1T检查FCC0的位,然后跳到相关位置。T表示如果为True则跳转。因此存在BC1F。
一些计算器使用reverse Polish notation。比如12,34的加法,先输入12和34,再输入+。因为计算器的实现就像栈一样。
http://challenges.re/60
http://challenges.re/61
求5个数的平均数。