上一篇介绍了浮点数的加减乘除运算指令,如果你的汇编程序是用于科学计算或者某些工程应用的话,就有必要了解更多的高级浮点运算指令...

    本文由zengl.com站长对
    http://pan.baidu.com/share/link?shareid=3717576860&uk=940392313 汇编教程英文版相应章节进行翻译得来。
    另外再附加一个英特尔英文手册的共享链接地址:
    http://pan.baidu.com/share/link?shareid=2345340326&uk=940392313 (在某些例子中会用到)

    本篇翻译对应汇编教程英文原著的第280页到第290页,对应原著第9章 (注意这里的页数不是页脚的页数,而是pdf电子档顶部,分页输入框中的页数,也就是包含了目录,前言部分的总页数,pdf电子档总页数是577,当前已翻译完的页数为290 / 577)。

Advanced Floating-Point Math 高级浮点数学运算:

    上一篇介绍了浮点数的加减乘除运算指令,如果你的汇编程序是用于科学计算或者某些工程应用的话,就有必要了解更多的高级浮点运算指令,可用的高级浮点指令如下表所示:

Instruction 
指令
Description 
描述
F2XM1 Computes 2 to the power of the value in ST0, minus 1 
计算2ST0-1的值,其中ST0为2的指数部分
FABS Computes the absolute value of the value in ST0 
计算ST0里值的绝对值
FCHS Changes the sign of the value in ST0 
改变ST0里值的正负符号
FCOS Computes the cosine of the value in ST0 
计算ST0栈顶寄存器值的余弦函数
FPATAN Computes the partial arctangent of the value in ST0 
计算arctan(ST1/ST0)即ST1与ST0的商的反正切函数值
FPREM Computes the partial remainders from dividing 
the value in ST0 
计算ST0除以ST1的余数
FPREM1 Computes the IEEE partial remainders from 
dividing the value in ST0 by the value in ST1 
采用IEEE标准计算ST0除以ST1的余数
FPTAN Computes the partial tangent of the value in ST0 
计算tanST0即ST0的正切函数值
FRNDINT Rounds the value in ST0 to the nearest integer 
将ST0的值舍入到最接近的整数值
FSCALE Computes ST0 to the ST1st power 
计算ST0 * 2ST1 即ST0与2的ST1次方的乘积
FSIN Computes the sine of the value in ST0 
计算ST0的正弦函数值
FSINCOS Computes both the sine and cosine of the value in ST0 
计算ST0的正弦函数和余弦函数值
FSQRT Computes the square root of the value in ST0 
计算ST0的平方根
FYL2X Computes the value ST1 * log ST0 (base 2 log) 
计算ST1*Log2ST0的值,即ST1乘以Log以2为底ST0的对数
FYL2XP1 Computes the value ST1 * log (ST0 + 1) (base 2 log) 
计算ST1*Log2(ST0+1)的值,即ST1乘以Log以2为底ST0加1的对数

    上面这些浮点指令很多都可以直接从字面上得出它的含义,下面就具体的介绍下这些指令的意义和用法。

Floating-point functions 浮点数学运算:

    FABS, FCHS, FRNDINT 以及 FSQRT指令用于完成一些简单的浮点运算,其中FABS指令用于计算ST(0)的绝对值,FCHS指令用于改变ST(0)值的符号位,FSQRT指令则用于计算ST(0)的平方根,下面的fpmath2.s程式就演示了这几个指令的用法:

# fpmath2.s - An example of the FABS, FCHS, and FSQRT instructions
.section .data
value1:
    .float 395.21
value2:
    .float -9145.290
value3:
    .float 64.0
.section .text
.globl _start
_start:
    nop
    finit
    flds value1
    fchs
    flds value2
    fabs
    flds value3
    fsqrt
    movl $1, %eax
    movl $0, %ebx
    int $0x80
 

    该程序调试输出的结果如下:

$ as -gstabs -o fpmath2.o fpmath2.s
$ ld -o fpmath2 fpmath2.o
$ gdb -q fpmath2

Reading symbols from /home/zengl/Downloads/asm_example/fpuAdv/fpmath2...done.
(gdb) break 20
Breakpoint 1 at 0x8048090: file fpmath2.s, line 20.
(gdb) r
Starting program: /home/zengl/Downloads/asm_example/fpuAdv/fpmath2

Breakpoint 1, _start () at fpmath2.s:20
20        movl $1, %eax

(gdb) info all
....................
st0            8    (raw 0x40028000000000000000)
st1            9145.2900390625    (raw 0x400c8ee5290000000000)
st2            -395.209991455078125    (raw 0xc007c59ae10000000000)
....................

(gdb)

    这里计算结果的顺序是和FLDS指令压栈的顺序相反的,ST0里的8是FSQRT指令对value3压入栈顶的值64进行平方根运算的结果,ST1为FABS指令对value2压入的值取绝对值的结果,ST2里的值则为FCHS指令修改符号位后的结果。

    FRNDINT指令用于将ST0里的值舍入为整数值,具体舍入的方式是由FPU控制寄存器里的Rounding control(10到11位的舍入控制位)来进行控制的(这个在前面FPU寄存器介绍篇里讲解过),一共有4种舍入方式,默认为00即舍入到最接近的值,还有01向下舍入(向负无穷大方向进行舍入),10向上舍入(向正无穷大方向进行舍入),以及11向零舍入。

    下面的roundtest.s程式就演示了FRNDINT指令的用法:

# roundtest.s - An example of the FRNDINT instruction
.section .data
value1:
    .float 3.65
rdown:
    .byte 0x7f, 0x07
rup:
    .byte 0x7f, 0x0b
.section .bss
    .lcomm result1, 4
    .lcomm result2, 4
    .lcomm result3, 4
.section .text
.globl _start
_start:
    nop
    finit
    flds value1
    frndint
    fists result1
    fldcw rdown
    flds value1
    frndint
    fists result2
    fldcw rup
    flds value1
    frndint
    fists result3
    movl $1, %eax
    movl $0, %ebx
    int $0x80
 

    上面代码中,一开始FINIT指令初始化FPU后,FPU控制寄存器里的默认值为0x037f,对应的10到11位的舍入控制位为00即舍入到最接近的值,然后代码会使用rdown和rup里的值来修改控制寄存器,其中rdown里的0x077f值对应的10到11位为01即向下舍入,rup里的0x0b7f值对应的10到11位为10即向上舍入,最后将不同舍入方式下的计算结果依次存储到result1、result2及result3的内存位置。

    在gdb调试器里可以通过info float命令来查看FPU控制寄存器的值,如下所示:

(gdb) info float
  R7: Empty   0x00000000000000000000
  R6: Empty   0x00000000000000000000
  R5: Empty   0x00000000000000000000
  R4: Empty   0x00000000000000000000
  R3: Empty   0x00000000000000000000
  R2: Empty   0x00000000000000000000
  R1: Empty   0x00000000000000000000
=>R0: Empty   0x00000000000000000000

Status Word:         0x0000                                            
                       TOP: 0
Control Word:        0x037f   IM DM ZM OM UM PM
                       PC: Extended Precision (64-bits)
                       RC: Round to nearest
Tag Word:            0xffff
.....................................

    上面输出里,Control Word控制字部分当为默认的0x037f时,对应的舍入方式就是Round to nearest(舍入到最接近的值)。

    在调试完所有浮点指令后,result1里存储的是舍入到最接近的结果:

(gdb) x/d &result1
0x80490c4 <result1>:    4
(gdb)

    由于需要舍入的浮点数3.65从四舍五入的角度最接近4,所以result1的值就为4。

    result2里存储的是向下舍入(向负无穷大方向舍入)的结果:

(gdb) x/d &result2
0x80490c8 <result2>:    3
(gdb)

    result3里存储的是向上舍入(向正无穷大方向舍入)的结果:

(gdb) x/d &result3
0x80490cc <result3>:    4
(gdb)

Partial remainders 浮点数取余运算:

    先来了解下浮点数取余的运算原理,例如求解20.65除以3.97的余数,就可以通过下面的几步来完成:
  1. 20.65 / 3.97 = 5.201511335, 进行舍入后得到的整数5就是商
  2. 5 * 3.97 = 19.85
  3. 20.65 – 19.85 = 0.8 (这里的计算结果0.8就是20.65除以3.97的余数)
    IA-32平台使用FPREM和FPREM1指令来完成这种浮点取余运算,这些指令指定ST0为被除数(如上面例子里的20.65),ST1为除数(如上面的3.97),取余的结果0.8会存储在ST0从而替换掉原来的被除数,但是这两个指令在执行时,ST0里的被除数在变为余数的过程中,ST0里值的二进制exponent指数的递减范围不能超过63,如果超过63就会生成一个Partial remainders(中间余数),然后需要使用这个中间余数再进行FPREM或FPREM1的取余运算,直到获取到最终的余数为止。

    在之前的 汇编数据处理 (三) 浮点数 章节里,介绍过浮点数在内存里的二进制科学计算形式,如标准单精度浮点数的格式如下:


图1

    上图里23到30位的Exponent部分就是指数部分,该指数的有效值范围为-126到128,当FPREM取余指令运算后,ST0里值的Exponent递减幅度超过63时,就只会得到一个中间余数。例如,我们将上例里的20.65替换为一个很大的单精度浮点数1288382893898492849284942.323294929492442,当然这个单精度浮点数会严重失真,这里只是做个简单的说明,在执行完一次FPREM指令后就会得到一个中间余数14863237120保存在ST0里,再次执行FPREM后,才会得到最终的余数1.36649.... 。

    既然会生成中间余数,这里就涉及到如何判断ST0里的值是中间余数还是最终余数的问题,前面介绍FPU寄存器时,提到过FPU状态寄存器里有个condition code bit 2(条件代码位2),如果该位处于set设置状态,则表示ST0里的值是个中间余数,需要继续求解,当该位清零时,则表示ST0里的值就是最终的余数,在汇编里可以通过TEST指令来测试该位的设置状态。

    现在再来看下FPREM和FPREM1两个指令的不同之处。英特尔最开始引入的是FPREM指令,该指令在进行舍入求商的过程中使用的舍入方式是向零舍入的方式,例如20.65 / 4.34 = 4.758064516134.75806451613按向零舍入的方式得到的商就是4,然后使用这个商去求解余数,得到的余数为3.29。

    不幸的是,后来IEEE
(电气电子工程师学会)又发布了一个标准,他使用的舍入方式是舍入到最接近的值的方式,因此,英特尔在保留原来FPREM指令的基础上,又引入了一个FPREM1指令,该指令在浮点数取余运算时,就采用IEEE标准来进行舍入求商的操作,例如上面的20.65 / 4.34 = 4.75806451613,4.75806451613按IEEE标准舍入到最接近的整数的话,商就会是5,使用5去求解余数,得到的结果就是 20.65 - 5 * 4.34 = -1.05 (-1.05就是余数)。

    至于是使用FPREM指令还是使用FPREM1指令,需要根据你自己的实际需求来决定,例如,GCC 4.3就是用FPREM指令来求解ST0 / ST1的余数的。

    下面的premtest.s程式就演示了使用FPREM1指令求解浮点余数的方法:

# premtest.s - An example of using the FPREM1 instruction
.section .data
value1:
    .float 20.65
value2:
    .float 3.97
.section .bss
    .lcomm result, 4
.section .text
.globl _start
_start:
    nop
    finit
    flds value2
    flds value1
loop:
    fprem1
    fstsw %ax
    testb $4, %ah
    jnz loop
    fsts result
    movl $1, %eax
    movl $0, %ebx
    int $0x80
 

    上面代码里,先将value2里的3.97除数压入栈,再将value1里的20.65被除数压入栈,这样ST0里就是20.65,ST1里就是3.97,接着使用FPREM1指令进行取余运算,取余的结果将保存在ST0,由于需要判断该结果是否只是一个中间余数,所以紧接着就用fstsw %ax将状态寄存器(状态字)里的值设置到AX寄存器,再由TEST指令测试condition code bit 2(条件代码位2),如果该位处于设置状态,说明是个中间余数,就通过jnz loop跳转到loop标签处,继续执行FPREM1指令,直到求解出最终余数为止,在得到最终余数后,将该余数通过fsts result指令存储到result内存位置。

    在汇编链接程序后,在gdb调试器里的输出结果如下:

(gdb) x/fw &result
0x80490a8 <result>:    0.799999475
(gdb)

    在得到ST0里的余数的时候,计算过程中通过舍入得到的商的低三位存储在状态寄存器的另外三个condition code bit(条件代码位)里,如下所示:
  • 商的位0存储在condition bit 1(条件代码位1)
  • 商的位1存储在condition bit 3(条件代码位3)
  • 商的位2存储在condition bit 0(条件代码位0)
    你需要手动提取和调整这些代码位,来得到商的低三位的值,例如,上面premtest程式在执行完FPREM1指令后,状态寄存器里的条件代码位如下:

(gdb) info float
  R7: Valid   0x4000fe147b0000000000 +3.970000028610229492      
=>R6: Valid   0x3ffeccccc40000000000 +0.7999994754791259766     
  R5: Empty   0x00000000000000000000
  R4: Empty   0x00000000000000000000
  R3: Empty   0x00000000000000000000
  R2: Empty   0x00000000000000000000
  R1: Empty   0x00000000000000000000
  R0: Empty   0x00000000000000000000

Status Word:         0x3300                                 C0 C1      
                       TOP: 6
.................................................
(gdb)

    上面输出里C0和C1处于设置状态,C3没有显示也就是清零状态,说明商的低三位为101即5 。

    FPREM和FPREM1指令只得到商的低三位,这看起来有点古怪,但这是有历史原因的,在过去旧的80287协处理器时代,FPTAN计算角度正切值的指令不能处理大于pi / 4即45度的角度,因此,在执行FPTAN指令前,就需要先用FPREM指令将角度针对pi / 4进行取余运算,得到的商的低三位作为坐标系里的象限值,而余数就可以进行FPTAN的正切函数运算,我们将360度的平面坐标系按pi / 4即45度可以划分为8个部分即8个象限,FPREM指令得到的商的低三位刚好可以表示8个数,每个数对应一个象限。

    到了80387协处理器后,FPTAN指令就没有这方面的限制了,所以现在FPREM得到的商值就很少被使用了。

Trigonometric functions 三角函数:

    FPU浮点计算的另一个重要功能就是计算三角函数,例如正弦,余弦及正切函数等,下面就具体的介绍下和这些运算相关的指令。

The FSIN and FCOS instructions FSIN和FCOS指令:

    这些基本的三角函数都有一个隐示的源操作数位于ST0里,当指令运算完后,结果存储在ST0中。

    这里需要注意的是,这些三角函数都是使用radians(弧度)作为源操作数的单位,如果你的应用程序里的数据是用的degrees(度)为单位的话,就必须在使用三角函数前,先将度转为弧度,可以使用下面的公式来进行转换:

radians = (degrees * pi) / 180

    上面这个公式可以使用下面的代码片段来实现:

flds degree1 # 将degree1内存里以degrees(度)为单位的值加载到ST0
fidivs val180 # 将ST0除以val180内存位置里的值180
fldpi              # 将pi加载到ST0,这样之前的degrees / 180的结果就会移入ST1
fmul %st(1), %st(0) # 将ST0里的pi乘以ST1里degrees / 180的值,得到对应的弧度值保存在ST0里
fsin               # 对ST0里的弧度值进行正弦函数运算

    下面的trigtest1.s程式完整的演示了FSIN,FCOS的用法以及度转弧度的方法:

# trigtest1.s - An example of using the FSIN and FCOS instructions
.section .data
degree1:
    .float 90.0
val180:
    .int 180
.section .bss
    .lcomm radian1, 4
    .lcomm result1, 4
    .lcomm result2, 4
.section .text
.globl _start
_start:
    nop
    finit
    flds degree1
    fidivs val180
    fldpi
    fmul %st(1), %st(0)
    fsts radian1
    fsin
    fsts result1
    flds radian1
    fcos
    fsts result2
    movl $1, %eax
    movl $0, %ebx
    int $0x80
 

    上面代码在将degree1里的90度转为弧度值后,将转换后的弧度值存储在radian1内存处,这样后面执行FCOS时就可以直接加载这个转换好的弧度值,代码中将FSIN指令计算的正弦值存储在result1处,将FCOS指令计算的余弦值存储在result2位置。

    trigtest1程序汇编链接后,在调试器里的输出结果如下:

$ as -gstabs -o trigtest1.o trigtest1.s
$ ld -o trigtest1 trigtest1.o
$ gdb -q trigtest1

Reading symbols from /home/zengl/Downloads/asm_example/fpuAdv/trigtest1...done.
...........................................................

(gdb) x/fw &result1
0x80490bc <result1>:    1
(gdb) x/fw &result2
0x80490c0 <result2>:    -4.37113883e-08
(gdb)

    从输出结果可以看到result1里存储的是90度的正弦值1,result2里为余弦值0,由于弧度在保存和加载的过程中存在精度丢失,所以result2得到的结果是一个很接近0的值。

    小提示:可以预先将pi /180的值计算并存储到FPU,这样度转弧度时速度要快很多。

The FSINCOS instruction FSINCOS指令:

    如果你需要同时计算出弧度的正弦和余弦值的话,可以使用FSINCOS指令,该指令运算完后,会先将sin正弦值替换掉ST0里的源弧度值,再将cos余弦值压入栈,这样结果就是cos余弦值将存储在ST0,sin正弦值将存储在ST1,下面的trigtest2.s程式就演示了该指令的用法:

# trigtest2.s - An example of using the FSINCOS instruction
.section .data
degree1:
    .float 90.0
val180:
    .int 180
.section .bss
    .lcomm sinresult, 4
    .lcomm cosresult, 4
.section .text
.globl _start
_start:
    nop
    finit
    flds degree1
    fidivs val180
    fldpi
    fmul %st(1), %st(0)
    fsincos
    fstps cosresult
    fsts sinresult
    movl $1, %eax
    movl $0, %ebx
    int $0x80
 

    trigtest2程式的结果将分别存储在cosresult和sinresult内存位置,如下所示:

(gdb) x/fw &cosresult
0x80490b0 <cosresult>:    -2.71050543e-20
(gdb) x/fw &sinresult
0x80490ac <sinresult>:    1
(gdb)

    cosresult里的值并非精确的0,但也非常接近了,sinresult里为正确的值1 。

The FPTAN and FPATAN instructions FPTAN和FPATAN指令:
[zengl pagebreak]
The FPTAN and FPATAN instructions FPTAN和FPATAN指令:

    FPTAN用于计算tanST0即ST0的正切函数值,它在计算完后,会先将tanST0的值压入寄存器栈,再将1压入栈,之所以这么做,是为了兼容80287的FPU协处理器,因为那时FSIN和FCOS指令还不可用,要计算这些值就需要用到tan正切值的倒数,FPTAN在计算后,只要再使用FDIV指令就可以得到这个倒数了(ST0里的1除以ST1里的tanST0值)。

    FPATAN指令用于计算反正切值,它需要的隐示源操作数有两个,一个ST0,一个ST1,该指令会计算arctan(ST1/ST0)即ST1与ST0的商的反正切函数值,并将结果存储在ST1,然后再将ST0弹出寄存器栈,这样结果就会由ST1往上挪入ST0,之所以要计算ST1与ST0商的反正切值,是因为这种形式下,当ST0为0时,ST1除以ST0就会是无穷大,这样就可以得到无穷大的反正切值。标准的ANSI C函数atan2(double x, double y)的内部实现也是类似的做法。

Logarithmic functions 对数函数:

    FPU里通过FYL2X和FYL2XP1指令来计算log以2为底的对数,其中FYL2X指令的运算方式如下:

ST(1) * log2(ST(0))

    FYL2XP1指令的运算方式如下:

ST(1) * log2(ST(0) + 1.0)

    FYL2XP1这个指令在Log后的数很接近一时,比FYL2X有较好的准确度。

    另外,还有个FSCALE指令可以对ST0的值进行缩放,该指令的运算方式如下:

ST(0) <-- ST(0) * 2RoundTowardZero(ST(1))

    它会将ST0乘以2的ST1次方,结果存储在ST0,当ST1为正数时,ST0的值就会扩大,当ST1为负数时,ST0的值就会缩小,下面的fscaletest.s程式就演示了FSCALE指令的用法:

# fscaletest.s - An example of the FSCALE instruction
.section .data
value:
    .float 10.0
scale1:
    .float 2.0
scale2:
    .float -2.0
.section .bss
    .lcomm result1, 4
    .lcomm result2, 4
.section .text
.globl _start
_start:
    nop
    finit
    flds scale1
    flds value
    fscale
    fsts result1
    flds scale2
    flds value
    fscale
    fsts result2
    movl $1, %eax
    movl $0, %ebx
    int $0x80
 

    上面代码先将scale1里的2.0压入栈,再将value内存里的10.0压入栈,这样ST0就为10,ST1就为2,经过FSCALE指令后,ST0里的结果就会变为10 * 22 = 40,该结果通过fsts result1指令存储到result1的内存位置,接着将scale2里的-2.0和value里的10依次压入栈,再执行一次FSCALE指令,ST0的结果就会变为10 * 2-2 = 2.5 ,该结果会被保存在result2的内存位置。

    fscaletest.s程式经过汇编链接后,调试输出的结果如下:

(gdb) x/fw &result1
0x80490b8 <result1>:    40
(gdb) x/fw &result2
0x80490bc <result2>:    2.5
(gdb)

    该输出结果和预期的一致。

    小提示:FSCALE指令的效果类似于整数运算里左移、右移指令的效果。

    尽管FYL2X和FYL2XP1指令只支持log以2为底的对数运算,但是你可以通过下面的公式来计算log以其他数为底的对数:

logbX = (1/log2b) * log2X

    上面的公式表示log以b为底X的对数,等于log以2为底b的对数的倒数,乘以log以2为底X的对数。

    下面的logtest.s程式就演示了计算log以10为底12的对数的方法:

# logtest.s - An example of using the FYL2X instruction
.section .data
value:
    .float 12.0
base:
    .float 10.0
.section .bss
    .lcomm result, 4
.section .text
.globl _start
_start:
    nop
    finit
    fld1
    flds base
    fyl2x
    fld1
    fdivp
    flds value
    fyl2x
    fsts result
    movl $1, %eax
    movl $0, %ebx
    int $0x80
 

    上面的代码先将1和base里的10压入栈,此时ST0就为10,ST1就为1,执行FYL2X时,就会得到1 * log210即log210的结果,然后再将1压入栈,通过FDIVP除法指令就可以得到log210的倒数,最后将value里的12压入栈,此时ST0为12,ST1为log210的倒数,再执行FYL2X指令,就能得到log210的倒数与log212的乘积,根据前面的公式可知,该结果就是log1012的值。

    logtest.s程式经过汇编链接后,调试输出的结果如下:

$ as -gstabs -o logtest.o logtest.s
$ ld -o logtest logtest.o
$ gdb -q logtest

Reading symbols from /home/zengl/Downloads/asm_example/fpuAdv/logtest...done.
.....................................

(gdb) x/fw &result
0x80490a8 <result>:    1.07918119
(gdb)

    输出情况和预期的值一致。

    OK,本篇就到这里,下一篇介绍FPU里浮点数的条件比较指令。

    转载请注明来源:www.zengl.com

    休息,休息一下 o(∩_∩)o~~

上下篇

下一篇: 高级数学运算 (四) 高级运算结束篇

上一篇: 高级数学运算 (二) 基础浮点运算

相关文章

优化汇编指令 (二)

汇编里使用Linux系统调用 (二)

汇编中使用文件 (一)

优化汇编指令 (一)

IA-32平台(二)

高级数学运算 (一) FPU寄存器介绍