文章目录

在Lua源代码的llimits.h头文件中有这么一段宏定义:

union luai_Cast { double l_d; LUA_INT32 l_p[2]; };

#define lua_number2int32(i,n,t) \
    { LUAI_EXTRAIEEE \
    volatile union luai_Cast u; u.l_d = (n) + 6755399441055744.0; \
    (i) = (t)u.l_p[LUA_IEEEENDIANLOC]; }

这lua_number2int32宏的作用是把双精度浮点数double转换成int。这个宏的工作流程是这样的:

  1. double类型的n与6755399441055744.0相加,赋值给u
  2. 将u的低4字节赋值给int类型的i

据我做的简单测试显示,在MacOS上该宏与简单地强制类型转换速度差不多,在windows 7 32位系统上分别用mingw和vc编译跑结果都显示该宏比强制类型转换速度要快,测试代码如下:

#include <cstdlib>
#include <ctime>
#include <iostream>
using namespace std;

union luai_Cast { double l_d; int l_p[2]; };

#define lua_number2int32(i,n,t) \
  { volatile union luai_Cast u; u.l_d = (n) + 6755399441055744.0; \
  (i) = (t)u.l_p[0]; }

#define NUMS 100000000

int main(int argc, char** argv) {
    srand( time( NULL ) );
    clock_t t;
    volatile int j;

    double* d = new double[NUMS];
    for(int i = 0; i < NUMS; i++) {
        d[i] = rand() * 1.0f;
    }

    t = clock();
    for(int i = 0; i < NUMS; i++) {
        j = (int)d[i];
    }
    t = clock() - t;
    cout << ((float)t) / CLOCKS_PER_SEC << endl;

    t = clock();
    for(int i = 0; i < NUMS; i++) {
        lua_number2int32(j, d[i], int);
    }
    t = clock() - t;
    cout << ((float)t) / CLOCKS_PER_SEC << endl;

    return 0;
}

这篇文章里会讨论该宏的工作原理,至于为什么会产生速度差异不在讨论范围。

双精度浮点数的表示在我之前的文章lua NaN trick有介绍,这里就不再累赘。我们来看一下8.75 = 1000.11 = 1.00011 * 2^3在big-endian系统下的表示:

0 | 10000000010 | 000110…0

其中第一部分1比特,第二部分11比特,第三部分52比特。如果将第三部分向右位移52 - 3 = 49位,那么该浮点数的低4字节表示的不就是int 8了吗!我们可以自己写位移操作,但这么做一来麻烦,二来没有利用FPU,那有没有既不用位移又能利用FPU的解决方案的呢?答案是必须有!我们来看一下8.75 + 256这个计算是怎么完成的:

  1. 8.75 = 1000.11 = 1.00011 * 2^3
  2. 256 = 1.0 * 2^8
  3. 指数对齐,小数向大数对齐,对齐后8.75 = 0.0000100011 * 2^8
  4. 相加0.0000100011 2^8 + 1.0 2^8 = 1.0000100011 * 2^8
  5. 标准化表示成1.xxxxx * 2^x形式

上述过程中指数对齐不就是一个位移操作吗,浮点数相加有FPU的情况下必须由FPU完成啊。基于这个思想我们将需要转型成int的浮点数加上1.0 2^52(二进制),那么经过上述的过程之后取该浮点数的低4字节就是转型后的数值。正的浮点数通过加上1.0 2^52来转成int是没有问题的,但是如果是负的浮点数呢?我们来看看-8.75 + 1.0 * 2^52会是什么结果:

  1. -8.75 = -1000.11 = -1.00011 * 2^3
  2. 1.0 * 2^52
  3. 指数对齐,-8.75 = -0.0…01000 * 2^52(小数点和1之间有48位0,小数部分这里直接舍弃,当然还有其他处理方式这里不讨论)
  4. 1.0 2^52 - 0.0…01000 2^52 = 0.1…1000 * 2^52(49位1)
  5. 标准化1.1…10000 * 2^51

我们可以看到如果在第4步就停止的话,那么转型结果是正确的,但是由于第5步的标准化过程,导致结果左移了1位。如果我们将1.0 2^52(二进制)换成1.1 2^52(二进制),那么在做减法的时候就能防止最高位变成0,在标准化的时候也就不会产生向左位移的结果。最后我们将1.1 * 2^52(二进制)转化为十进制就是上文那个宏中那个神奇的数字6755399441055744.0了!

文章目录

举个栗子……