持续一年的无意义做个了结吧!其实我也不知道为什么要发出来,有什么意义?估计很多人又要说装逼吧,随意吧。反正我就当作是玩了一场智力游戏。
没错,高级语言已经帮你弄好了,没必要的。但是ASM下没有哦,只能自己实现了呢。
先说说我对浮点数了理解吧,我没有看完IEEE 754,太长了,也太深奥了。所以确实了解不深,某些概念上可能有误,不过对于简单的浮点数处理已经够了。
好,来说说浮点数。浮点数最直观的感受是它的长度,最常见的有单双精度,不常见的有半精度,四倍精度,八倍精度。还有双扩展格式等。常见的规格适用几乎于一切编译器,对应的声明是float和double,但是还有一种特殊的long double,属于双扩展格式,如果编译器支持,他是80位的,不支持就可能会映射到double去。至于128位和256位的四倍精度,八倍精度,基本用不到。半精度用于对性能要求很高但对结果精度要求低的场合,比如GPU。
虽然浮点数格式繁多,但是基本原理是一样的。首先一句话建立初步印象:浮点数在计算机中以二进制的科学记数法来表示。所以,对于常见的float,double浮点由:符号位31bit/63bit,阶码23bit-30bit/52bit-62bit,及尾数0bit-22bit/0bit-50bit组成
符号位:正数为0,负数为1;
阶码部分:单精度浮点8有位阶码,代表科学记数法的指数部分,不过它的值并不是指数的值,而是再加一个偏移量:0x7F,对于双精度浮点,则是11位阶码,偏移量为0x3FF。
尾数部分:对于科学记数法,要求有效数字的小数点在最高一位数之后,比如1.1010111001,跟十进制科学记数法一样,小数点前面必须只有一位不为0的数,如50*10^2是错的,必须是5*10^3。因此二进制下,小数点前面总是是1可以略去不存,所以尾数就是有效数字去掉小数点前面的1.
根据这个我们做一个推断:6.125该如何用float表示?
首先,转化为二进制数为:110.001转化为标准科学计数法为1.10001*2^2。然后开始生成浮点数:首先,是正数,所以符号位为0;其次,指数部分为2,所以阶码部分为0x7F+0x02=0x81;最后,有效数字为1.10001去掉小数点前面的1,只取小数部分:10001然后后面填充0凑够23位。把上述结果转化为二进制得:0|10000001|(1)10001000000000000000000其中:|表示结构分割,()表示1被舍弃。十六进制为:0x40C40000,将结果代入浮点转整形程序得:6.125,推论正确!
单双精度浮点能表示的范围远比相同位数的整形数大得多。但是相比于整形数,某些位用作表示符号和阶码,所以有效数字位数降低,即牺牲精度换表示范围。对于float,最大值是尾数全1,阶码为254时,为:1.11111111111111111111111*2^126;对于Uint32,则为:2^32-1;甚至对于Uint64,则:2^64-1也没能超过它,更不用提double了!
现在抽出其中一个函数进行讲解:
这是float的乘法,输入两个float的操作数,输出float是它们的积。但是我写的这些函数对于特殊情况是不支持的,比如说溢出,无穷,不是一个数的异常,只对0这个特殊情况支持。一般对于正确的算法不会涉及到特殊情况所以为了简化就不做这些判断及处理了。
执行思路:前4条指令实现了对阶码的提取和判0操作,因为只支持0的特殊情况,所以阶码为0一律认为输入数据有0,从而直接返回0;当阶码不为0后接下来的2条指令先将其相加再减去一个固定偏移量,因为阶码是在23-30bit的,所以要减去的固定偏移量为0x7F<<0x17=0x3F800000,这个作为结果的阶码,不难理解,想一想对于科学记数法的乘法该如何做你就明白了!随后进行符号位计算,接下来的2条指令将两个输入参数异或运算,结果只关心最高位31bit,异或的逻辑刚好符合一正一负得负,负负得正,正正不变得原则。随后将刚计算出来的阶码替换异或后的结果除31位的所有位,这样,输出符号位和阶码就有了,顺带清0了23位宽的尾数位置0-22bit,为下文埋下伏笔。接下来的3条指令实现了尾数还原为标准有效数字并左移8位的任务,注意这里出现了复合指令:ORR R1,R3,R1,LSL#0x08 这个是ARM的特性,部分算术逻辑运算可以先把一个操作数进行预移位处理,再进行后续操作。该条指令逻辑为:先将R1左移8位,将左移后的中间值与R3完成逻辑与运算,并输出到R1.完成有效数字的移位提取后的一条长乘法指令取得63或64位宽结果,而float只有23位宽尾数,所以要取积的最高一位丢弃,得到23位宽的结果尾数,这个任务有接下来的部分完成。接下来的移位只保留31bit的比特位,根据乘积位宽是63或64,R1为0,或者1.如果是64位宽则阶码加1,正好通过将R1移位23位后加到阶码实现,如果乘积是63位宽,阶码相同操作不加1.然后接下来把R1加上7,作为乘积高位寄存器R3要移动的位数,如果是64位宽的结果,R3要移动8位保留24位宽的结果,移位过程更新标志位,为后续修约操作做铺垫。如果是63位宽结果,一样推理。移位完成后,清除结果的23bit正好保留了23位宽的尾数,然后把它与之前算的阶码与符号部分相加,再加上标志位后输出。移位更新标志位再做进位加法是四舍五入的操作。
这样讲解是不是非常复杂,没错,所以根本不可能把所有的函数思路全部讲解,有闲得无聊的可以下载工程文件自己慢慢参悟。
随后,做算法功能正确性检验,直至目前,我并没有完成全部函数的Debug操作,仅仅初步查看了少部分函数,所以做出声明:警告,这库里面的所有函数只经过理论上的推演,并没有实际进行过大量而详尽的测试,所以仍然可能隐含Bug,并且没有人会为它的结果负责。可以作为参考或者经测试无误后使用。由于某些原因,这个库在相当长的一段时间不会再维护,但是不会被放弃,它终将变成最希望的模样。
为了方便起见,你可以通过适当方法在C环境中调用ASM函数,如图所示: 在C环境下,运算结果直接通过查看窗口观察到十进制结果,而不是ASM环境下你还要算半天十六进制结果。
关于这个库的其它说明:
函数参数规则:
对于一个参数:
32bit操作数32bit结果时:Operand1 -> R0; Result -> R0.
64bit操作数32bit结果时:Operand1_L -> R0, Operand1_H -> R1; Result -> R0.
32bit操作数64bit结果时:Operand1 -> R0; ResultL-> R0, ResultH -> R1.
64bit操作数64bit结果时:Operand1_L -> R0, Operand1H -> R1; Result_L-> R0, Result_H -> R1.
对于两个参数:
32bit操作数32bit结果时:Operand1 -> R0, Operand2 -> R1; Result -> R0.
64bit操作数32bit结果时:Operand1_L -> R0, Operand1H -> R1, Operand2L-> R2, Operand2_H -> R3; Result -> R0.
32bit操作数64bit结果时:Operand1 -> R0, Operand2 -> R1; Result_L-> R0, Result_H -> R1.
64bit操作数64bit结果时:Operand1_L -> R0, Operand1_H -> R1, Operand2_L-> R2, Operand2_H -> R3; Result_L-> R0, Result_H -> R1.
例如,计算1+1=2,全部为单精度:
LDR R0,=0x3F800000
MOVS R1,R0
BL FloatAdd
结果在R0内;
计算1+1,输入单精度,输出双精度:<strike></strike>
LDR R0,=0x3F800000
MOVS R1,R0
BL FloatAddToDouble
结果高位R1,低位R0.
<strike></strike>
符合小端模式。
<strike></strike>
另外,对于减法和除法,总是:
R0-R1=R0
R0-R1=[R1,R0]
R0/R1=R0
R0/R1=[R1,R0]
[R1,R0]-[R3,R2]=[R1,R0]
[R1,R0]/[R3,R2]=[R1,R0]
结论:游戏一场,不堪回首,欲生欲死,柳暗花明。工程文件:
Test.7z
25.51KB
7Z
67次下载
<strike></strike><strike></strike>
<strike></strike><strike></strike>
200字以内,仅用于支线交流,主线讨论请采用回复功能。