腾讯游戏开发精粹
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

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(),与汇编版本的功能一致。