c type conversion
曾经一直好奇c编译器如何做的类型转换, 最近研究了几天, 有了个大概的了解, 这里做个总结:
一.整型间转换
整型间的转换比较简单, 如果字长变化, 只需进行扩展或者截断
这方面gcc和msvc编译器做法大同小异, 就不分开讨论了, 就以gcc为例
1. 扩展:
比如int8到int32的扩展, 就对应有movsbl, movzbl指令 (s表示符号位扩展, 用于有符号数; z表示零扩展, 用于无符号数)
如此之类的指令还有movswl, movzwl, movsbw, movzbw.
有符号扩展还包括cbtw, cwtl, cwtd, cltd, 分别对应intel汇编的cbw,cwde, cwd, cdq
void char2int()
{
char c = 0xff;
int i = c;
}
gcc生成的汇编代码:
...
movb $-1, -1(%ebp) @ char c = 0xff;
movsbl -1(%ebp), %eax @ char to int
movl %eax, -8(%ebp) @ int i = c;
...
2. 截断:
截断更简单, 直接取通用寄存器的低8位, 比如使用movb指令取eax中的al, 就可以将int32截断为int8
void int2char()
{
int i = 0xffffffff;
char c = i;
}
gcc生成的汇编代码:
...
movl $-1, -4(%ebp) @ int i = 0xffffffff;
movl -4(%ebp), %eax @ int to char
movb %al, -5(%ebp) @ char c = i;
...
3. singed与unsigned间转换
一个简单的规则, 相同长度的signed与unsigned转换, 位模式不变.
顺便提一下, 如果同时存在位的扩展 和 signed/unsigned间转换则. 则扩展在前, signed/unsigned转换在后.
char c = -1; unsigned int u = c; // u == 0xffffffff, not 0xff
二. 整型与浮点数互转
如果是整型和浮点类型之间相互转换, 编译器又会如何做?
首先推荐一篇文章, 是先前查资料时发现的, 觉得讲的很详细: 浮点数到整数的快速转换
链接的文章是转载的, 原文链接已经404了, 从优化和算法的角度上讲不失为一篇好文.
但仔细看文章转载的时间是06年, 已经很久远了, 原文发表的时间就更久远了
这期间各种处理器的架构都发生了很多变化, intel处理器也增加了不少指令(比如我的处理器i5 2500k, 就支持很多扩展指令: MMX, SSE, SSE2, SSE3, SSSE3, SSE4.1, SSE4.2, EM64T, VT-x, AES, AVX)
接下来就看看更现代一点的编译器是怎么做的
1. Linux x86_64 gcc
这个特定系统(加编译器)中, 整型和浮点型间转换就是靠这几个指令
float to int: cvttss2si
int to float: cvtsi2ss
double to int: cvttsd2si
int to double: cvtsi2sd
可以很直观地得出一个结论: 64位linux中大量使用sse指令, 从硬件角度讲x86_64构架是完全支持sse的, 所以gcc有能力这么做.
2. windows vc (vc express 2010)
a. 整数 => 浮点数:
fild
fstp
b. 浮点数 => 整数
vc用一个通用的函数_ftol2_sse完成了浮点数到整数的转换
反汇编跟踪, 在我的机器上执行到的代码如下:
... fld dword ptr [f] call @ILT+200(__ftol2_sse) (0F710CDh) ... __ftol2_sse: jmp _ftol2_sse (0F71620h) ... _ftol2_sse: cmp dword ptr [___sse2_available (0F7757Ch)],0 je _ftol2 (0F71656h) push ebp mov ebp,esp sub esp,8 and esp,0FFFFFFF8h fstp qword ptr [esp] cvttsd2si eax,mmword ptr [esp] leave ret
大致意思就是先把float/double load到fpu的栈上, 再以双精度的值弹出来, 这样无论原来的浮点数是什么精度, 都变成了double, 然后再使用cvttsd2si 转换成整数
用简短的代码表示, 等效于如下过程:
void float2int()
{
float f = 3.14f; // double f = 3.14;
long long tmp;
int i;
__asm
{
fld f // 将f压入FPU中寄存器组成的栈
fstp tmp // 将f弹出到64位的tmp中, 此时tmp中保存的是双精度的3.14
cvttsd2si eax, tmp
mov i, eax
}
printf("%dn",i);
}
上面的float f = 3.14 也可以换成 double f = 3.14
我好奇的是为什么vc不像gcc那样直接使用指令, 而是不惜增加额外开销使用函数来转换? VC8.0类型转换分析(双精度浮点转整型)这篇文章也指出__ftol2_sse函数即使编译为Release版本也没有大幅度的优化.
我想也许是出于代码复用和兼容旧机器的角度来考虑的吧. 一是float和double都可以使用相同的函数, 二是兼容了不支持sse2指令集的机器( 不支持sse2则跳转到_ftol2). 然而这也侧面体现了linux的简洁高效所在.