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的简洁高效所在.