2.3 定点数的四则运算
考虑两个定点数a,b的四则运算,它们对应的整数分别是f(a),f(b),暂不考虑溢出等问题。
这意味着两个定点数的和/差,就是这两个定点数对应的整数的和/差表示的定点数。
其中,f(a)f(b)是两个定点数对应的整数的乘积,是这个乘积再除以2n得到的整数所表示的定点数。
其中,是两个定点数对应的整数的商,是这个商再乘以2n得到的整数所表示的定点数。而乘以、除以2n的运算可以方便地通过移位操作来实现。
2.3.1 加法与减法
浮点数的加减需要先对齐阶码(相当对齐小数点),然后再相加减。而定点数的小数点是对齐的,按照我们之前讲的原理,可以直接使用对应的整数的加减法。
2.3.2 乘法
浮点数的乘法是通过整数部分相乘、阶码相加实现的。
对于32位定点数,按照前面所讲的原理可以实现为
std::int32_t a; std::int32_t b; std::int32_t value = (std::int64_t(a) * b) >> 10;
这里做右移44.20(64)→32.10(32)时存在数据信息丢失和溢出的可能。在使用时要注意这一点。
对于64位定点数,直接将两个定点数的内部64位整数相乘,需要一个128位的整数才能完整表示结果。
对于GCC,可以使用_int128_t 128位类型,能直接使用128位乘法。
_int64_t value = (_int128_t(a) * b) >> 32;
而对于VC,目前还没有128位整数类型,可以使用intrinsic function _mull128来计算,并用两个std::int64_t来存储结果。
std::int64_t retHigh = 0; std::int64_t retLow = _mul128(a, b, &retHigh);
在函数的第三个参数中传入高64位变量的地址,即可以得出计算结果的高64位。
可以自定义简单的128位数据来存储内部64位整数相乘的完整结果。例如:
struct Multiply128 { std::uint64_t Low; std::int64_t High; }
2.3.3 除法
按照前面所讲的原理,做除法运算需要乘以232,为避免溢出需要引入128位的运算。对于GCC可以直接使用128位整数除法:
_int64_t value = (_int128_t(a) << 32) / b;
但对于VC就不行了,在intrinsic function中没有128位整数除法。但是可以用汇编代码来实现。
在C++中声明:
extern "C" std::int64_t _Div128_64( std::int64_t a_low, std::int64_t a_high, std::int64_t b, std::int64_t* ret );
这里把a_low的值传给寄存器rcx,把a_high的值传给rdx,把b的值传给r8,把ret的指针传给r9。
用汇编代码实现:
.code Div128_64 proc mov rax, rcx ; 将a_low的值由rcx传给rax idiv r8 ; rdx - rax(128位被除数) / r8(除数) = rdx(余数), rax(商) mov[r9], rdx ; 通过指针传出余数 ret ; 返回商 ret; Div128_64 endp END
需要注意的是,除出来的商不能超过64位,否则CPU会报出异常。
在审稿阶段,笔者发现最新版本的Visual Studio 2019已经提供了128位整数除法的intrinsic function _div128(),与汇编版本的功能一致。