将双精度整数舍入为32位int的快速方法


169

在阅读Lua的源代码时,我注意到Lua使用a将a macrodouble入为32位int。我提取了macro,它看起来像这样:

union i_cast {double d; int i[2]};
#define double2int(i, d, t)  \
    {volatile union i_cast u; u.d = (d) + 6755399441055744.0; \
    (i) = (t)u.i[ENDIANLOC];}

这里ENDIANLOC定义为endianness0对于小端,1对于大端。Lua会小心处理字节顺序。t代表整数类型,例如intunsigned int

我做了一些研究,有一个更简单的格式macro使用了相同的想法:

#define double2int(i, d) \
    {double t = ((d) + 6755399441055744.0); i = *((int *)(&t));}

或采用C ++样式:

inline int double2int(double d)
{
    d += 6755399441055744.0;
    return reinterpret_cast<int&>(d);
}

此技巧可以在使用IEEE 754的任何计算机上使用(这意味着当今几乎所有计算机)。它适用于正数和负数,并且四舍五入遵循Banker规则。(这并不令人惊讶,因为它遵循IEEE754。)

我写了一个小程序来测试它:

int main()
{
    double d = -12345678.9;
    int i;
    double2int(i, d)
    printf("%d\n", i);
    return 0;
}

并按预期输出-12345679。

我想详细介绍这个棘手的macro工作原理。幻数6755399441055744.0实际上是2^51 + 2^521.5 * 2^52,并且1.5以二进制形式可以表示为1.1。当任何32位整数添加到该幻数上时,我从这里迷失了方向。这个技巧如何运作?

PS:这是在Lua源代码Llimits.h中

更新

  1. 正如@Mysticial指出的那样,此方法不限于32位intint只要数字在2 ^ 52范围内,它也可以扩展为64位。(macro需要一些修改。)
  2. 一些材料说这种方法不能在Direct3D中使用
  3. 使用适用于x86的Microsoft汇编程序时,macro编写的速度甚至更快assembly(这也摘自Lua源码):

    #define double2int(i,n)  __asm {__asm fld n   __asm fistp i}
  4. 单精度数字有一个相似的幻数: 1.5 * 2 ^23


3
“快速”相比是什么?
科里·纳尔逊

3
@CoryNelson快速相比,一个简单的演员。实际上,使用SSE内部函数正确实现此方法比强制转换要快一百倍。(这会调用讨厌的函数调用,而该调用会调用相当昂贵的转换代码)
Mysticial 2013年

2
是的-我可以看到它比以前快ftoi。但是,如果您正在谈论SSE,为什么不只使用一条指令CVTTSD2SI呢?
科里·纳尔逊

3
@tmyklebu可以使用的许多用例double -> int64确实在2^52范围内。这些在使用浮点FFT执行整数卷积时尤其常见。
Mysticial

7
@MSalters不一定正确。强制转换必须符合语言规范-包括对溢出和NAN情况的正确处理。(或在IB或UB情况下编译器指定的任何内容)这些检查往往非常昂贵。这个问题中提到的技巧完全忽略了这种极端情况。因此,如果您想要速度并且您的应用程序不在乎(或从未遇到过)这种极端情况,那么此hack非常适合。
Mysticial 2013年

Answers:


161

A double表示如下:

双重代表

可以看成是两个32位整数;现在,int代码的所有版本(假设是32位int)中所采用的都是图中的右侧,因此最后要做的只是取最低的32位尾数。


现在,到魔幻数字;正如您正确说的那样,6755399441055744是2 ^ 51 + 2 ^ 52; 添加这样的数字将迫使其double进入2 ^ 52到2 ^ 53之间的“甜蜜范围”,正如Wikipedia 在此处解释的那样,它具有一个有趣的属性:

在2 52 = 4,503,599,627,370,496和2 53 = 9,007,199,254,740,992之间,可表示的数字恰好是整数

这是因为尾数为52位宽。

关于添加2 51 +2 52的另一个有趣的事实是,它仅影响两个最高位的尾数-无论如何,这些尾数都将被丢弃,因为我们仅采用了最低的32位。


最后但并非最不重要的:标志。

IEEE 754浮点使用幅度和符号表示,而“标准”机器上的整数使用2的补码算法;这里如何处理?

我们只讨论了正整数;现在假设我们正在处理一个32位可表示范围内的负数int,因此(绝对值)比(-2 ^ 31 + 1)小;称呼它-a。通过加上幻数,可以明显地使此数字为正,结果值为2 52 +2 51 +(-a)。

现在,如果我们以2的补码表示法解释尾数,将会得到什么?它必须是(2 52 +2 51)和(-a)的2的补码和的结果。同样,第一项仅影响高两位,保留在位0〜50中的是(-a)的2的补码表示(再次减去高两位)。

由于仅通过切掉左侧的多余位就可以将2的补码数减小为较小的宽度,因此采用低32位可以正确地(-a)用32的2的补码算术。


“”“关于添加2 ^ 51 + 2 ^ 52的另一个有趣的事实是,它只影响两个最高位的尾数-由于我们只使用了最低的32位,所以无论如何都将丢弃尾数。”“”那是什么?加这个可能会移动所有的尾数!
YvesgereY 2013年

@John:当然,添加它们的全部目的是迫使该值在该范围内,这显然可能导致尾数相对于原始值发生偏移(在其他值之间)。我在这里所说的是,一旦您处于该范围内,与相应的53位整数不同的唯一位就是51和52位,无论如何它们都将被丢弃。
Matteo Italia

2
对于那些想要转换为int64_t您的人,可以通过将尾数先左后右移动13位来实现。这将清除“魔术”数中的指数和两位,但会保留符号并将其传播到整个64位带符号整数。union { double d; int64_t l; } magic; magic.d = input + 6755399441055744.0; magic.l <<= 13; magic.l >>= 13;
Wojciech Migda '16
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.