浮点数到整数的快速转换。在计算机图形运算中,常常要将浮点数转换为整数,例如在图像的光栅化阶段,就要执行大量的类型转换,以便将浮点数表示的坐标转化为整数表示的屏幕坐标。
—————————————————————————————-
//
//
//
//
int_val
—————————————————————————————-
嘿嘿,很高兴你居然和我一样单纯!这个操作实在是太TINY了,以至于我从来没想过它是怎么实现的,直到某天某个家伙跟我说,不要使用标准C类型转换,因为那太慢了!我当时的震惊不下于倒霉的冒险者遇上了龙。
标准C类型转换最大的优点是,它是独立于平台的,无论是在X86上跑,还是在PowerPC上跑,你什么都不用担心,编译器会为你搞定一切。而这也恰恰是它最大的缺点——严重依赖于编译器的实现。而实际测试表明,编译器所生成的代码,其速度实在不尽人意。
一个替代的方法是直接对数据位进行操作。如果你对IEEE浮点数的表示法比较熟悉的话(如果你压根什么都不知道,请先查阅文末附录中的资料),这是显而易见的。它提取指数和尾数,然后对尾数执行移位操作。代码如下:
—————————————————————————————-
//
//
//
//
void
{
ival
//
//
int
//
//
//
int
if
ival
else
ival
//
if
ival
}
—————————————————————————————-
该函数有一个BUG,那就是当fval=0时,返回值是2。原因是对于语句mantissa>>exponent,编译器使用了循环移位指令。解决方法是要么对0作特殊处理,要么直接用汇编来实现。
这个函数比标准的C转换要快,而且由于整个过程只使用了整数运算,可以和FPU并行运行。但缺点是,(1)依赖于硬件平台。例如根据小尾和大尾顺序 的不同,要相应地修改函数。(2)对于float和double要使用不同的实现,因为二者的数据位不同。(3)对于float,只能保留24位有效值, 尽管int有32位。
更快的方法是使用FPU指令FISTP,它将栈中的浮点数弹出并保存为整数:
—————————————————————————————-
//
//
//
//
inline
{
_asm
{
fld fval
mov edx,
fistp dword
}
}
—————————————————————————————-
很 好,无论速度还是精度似乎都相当令人满意。但如果换一个角度来看的话,fistp指令需要6个cycle,而浮点数乘法才仅仅需要3个cycle!更糟的 是,当fistp运行的时候,它必须占用FPU,也就是说,其他的浮点运算将不能执行。仅仅为了一次类型转换操作就要付出如此大的代价,光想想就觉得心 疼。
当然,它也有很多优点:更快的速度,更精确的数值(四舍五入到偶数),更强的适用性(float和double均可)。要注意的是,FPU默认的四 舍五入到偶数(round
除此之外,还有更好,更快的方法吗?有的,那就是华丽的
请看下面的函数:
—————————————————————————————-
//
//
//
//
inline
{
static
dval
val
}
—————————————————————————————-
快,这就是它最伟大的地方!你所需要的仅仅是一次浮点数加法,你还能再奢望些什么呢?
当然,绝对不要使用莫名其妙的代码,现在马上就让我们来看看它是怎么变的戏法。
首先,我们要搞清楚FPU是怎样执行浮点数加法的。考虑一下8.75加2^23。8.75的二进制表示是1000.11,转化为IEEE标准浮点数格式是1.00011*2^3。假设二者均是32位的float,它们在内存中的存储为:
—————————————————————————————-
符号位(31),
8.75:
2^23:
—————————————————————————————-
FPU所执行的操作是:(1)提升指数较小者的指数,使二者指数相同;(2)将二者的尾数相加;(3)将结果规整为标准格式。让我们具体来看看整个过程:
—————————————————————————————-
1,提升8.75的指数,尾数要相应地右移23-3=20位:
8.75
2,将二者相加。必须注意的是,
8.75:
2^23:
0,
3,将规整为标准格式:
别忘了2^23还有个前导1,所以结果是规整的,无需执行任何操作
—————————————————————————————-
你发现什么了吗?不错,将结果的尾数部分提取出来,正是int(8.75)!是的,magic
但是别高兴得太早,让我们来看看负数的情况。以-8.75为例,-8.75加2^23相当于2^23减去8.75,过程如下:
—————————————————————————————-
2^23:
8.75:
0,
—————————————————————————————-
很好,尾数部分正是int(-8.75)=-8的补码。然而,2^23的前导1在减法过程中被借位了,所以结果的尾数前面并没有1,FPU将执行规整操作,最后我们得到的是:
—————————————————————————————-
0,
—————————————————————————————-
功 亏一篑!等等,如果将2^23的尾数的最高位置1,那么在减法过程中不就可以保全前导1了吗?完全正确,这就是我们需要的。所以最后的 magic
然而,我们要清楚这个方法的一些局限性。首先,在将结果的float值保存为整数时,我们需要使用某些mask值将22-31位屏蔽掉。其 次,float只能保有最多23位的有效值,在将尾数最高位置1后,有效值更是降到了22位,这意味着我们对大于2^23-1的数值无能为力。
相比之下,如果我们只处理double,那么所有的问题就都迎刃而解了。首先,double的指数位,符号位和尾数的最高位全部都在高32位,无需屏蔽操作。其次,double有多达52位的尾数,对于32位的int来说,实在是太奢侈了!
用于double的magic
根据同样的原理,我们还可以计算出将float和double转化为定点数的magic
另外,不得不再次重申的是,对于在最后的结果中被移除掉的数值,FPU会将其四舍五入到偶数。然而,有时我们确实需要像floor和ceil那样的操作,那该怎么办呢?很简单,将原数加上或减去一个修正值就可以了,如下所示:
—————————————————————————————-
//
static
//
inline
{
RoundToInt64(val,dval-delta);
}
//
inline
{
RoundToInt64(val,dval+delta);
}
—————————————————————————————-
这 个世界上没有免费的午餐。我们获得了速度,相对应地,也必须付出一些代价,那就是可移植性。上述方法全都基于以下几个假设:(1)在x86上跑;(2)符 合IEEE的浮点数标准;(3)int为32位,float为32位,double为64位。局限性其实是蛮大的,相比之下,int_val= (int)float_val就要省事多了。当然,你也可以使用条件编译。
最后,让我们来看两组实际的测试数值。这份报告来自于Sree
—————————————————————————————-
平台:Pentium
64位浮点数到32位整数的转换:
int(f):
fistp:
magic
64位浮点数到16-16定点数的转换:
int(f*65536):
fistp:
magic
floor函数:
ANSI
magic
—————————————————————————————-
平台:Pentium
64位浮点数到32位整数的转换:
int(f):
fistp:
magic
64位浮点数到16-16定点数的转换:
int(f*65536):
fistp:
magic
floor函数:
ANSI
magic
—————————————————————————————-
性 能的差距实在令人惊讶,不是吗?我想说的是,写编译器的家伙绝对不是傻瓜(恐怕比你我都要聪明得多),他们当然知道所有这些优秀的算法。但为什么编译器的 表现会如此糟糕呢?其中一个理由是,为了使浮点数运算尽可能精确和迅速,IEEE在算法的选择上有很多的限制。而另一方面,IEEE的舍入规则(四舍五入 到偶数)尽管从维持浮点数的连贯性上来看非常棒,但却不符合ANSI