解释了将double转换为32位int的快速方法

时间:2013-06-11 02:01:34

标签: c++ c performance floating-point

在阅读Lua's源代码时,我注意到Lua使用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's Rule。 (这并不令人惊讶,因为它遵循IEEE 754。)

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

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

按预期输出-12345679。

我想详细介绍这个棘手的macro是如何工作的。幻数6755399441055744.0实际上是2^51 + 2^52,或1.5 * 2^52,而二进制中的1.5可以表示为1.1。当任何32位整数被添加到这个神奇数字时,好吧,我从这里迷路了。这个技巧如何运作?

P.S:这是Lua源代码Llimits.h

更新

  1. 正如@Mysticial所指出的,这种方法并不局限于32位int, 只要数字在,它也可以扩展为64位int 范围为2 ^ 52。 (macro需要进行一些修改。)
  2. 有些材料称此方法无法在Direct3D中使用。
  3. 使用Microsoft汇编程序for x86时,有一个偶数 写在macro中的更快assembly(这也是从Lua源中提取的):

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

3 个答案:

答案 0 :(得分:160)

double表示如下:

double representation

它可以看作是两个32位整数;现在,代码的所有版本中的int(假设它是一个32位int)是图中右侧的那个,所以你最后所做的只是服用尾数的最低32位。


现在,到神奇的数字;正如你所说,6755399441055744是2 ^ 51 + 2 ^ 52;添加这样的数字迫使double进入2 ^ 52和2 ^ 53之间的“甜蜜范围”,正如维基百科here所解释的那样,它具有一个有趣的属性:

  

在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的补码和(2 52 +2 51 )和(-a)的结果。同样,第一项仅影响高两位,0~50位中剩余的是(-a)的2的补码表示(再次,减去高位两位)。

由于只是通过切掉左边的额外位来将2的补码数减少到更小的宽度,所以取下32位可以正确地给出(-a)32位,2的补码算法。

答案 1 :(得分:2)

这种“技巧”来自较旧的x86处理器,使用8087指令/接口进行浮点运算。在这些计算机上,有一条指令可将浮点转换为整数“ fist”,但它使用当前的fp舍入模式。不幸的是,C规范要求fp-> int转换截断为零,而所有其他fp操作都舍入为最接近的值,因此执行
fp-> int转换首先需要更改fp舍入模式,然后进行拳头操作,然后再恢复fp舍入模式。

现在在最初的8086/8087上还算不错,但是在后来开始获得超标量和无序执行的处理器上,更改fp舍入模式通常会串行化CPU内核,并且相当昂贵。因此,在奔腾III或奔腾IV等CPU上,总体成本非常高-正常的fp-> int转换要比此add + store + load技巧贵10倍甚至更多。

但是,在x86-64上,浮点数是通过xmm指令完成的,转换的成本是
fp-> int很小,因此这种“优化”可能比正常转换要慢。

答案 2 :(得分:-1)

这是上述Lua技巧的更简单实现:

/**
 * Round to the nearest integer.
 * for tie-breaks: round half to even (bankers' rounding)
 * Only works for inputs in the range: [-2^51, 2^51]
 */
inline double rint(double d)
{
    double x = 6755399441055744.0;  // 2^51 + 2^52
    return d + x - x;
}

该技巧适用于绝对值<2 ^ 51的数字。

这是一个用于测试它的小程序:ideone.com

#include <cstdio>

int main()
{
    // round to nearest integer
    printf("%.1f, %.1f\n", rint(-12345678.3), rint(-12345678.9));

    // test tie-breaking rule
    printf("%.1f, %.1f, %.1f, %.1f\n", rint(-24.5), rint(-23.5), rint(23.5), rint(24.5));      
    return 0;
}

// output:
// -12345678.0, -12345679.0
// -24.0, -24.0, 24.0, 24.0