最近,我们在一些旧代码中发现了奇怪的行为。这段代码已经工作了很长时间,但在某些平台(XBox 360,PowerPC)上打破了编译器优化最大化。通常,我怀疑未定义的行为。
代码看起来大致如下:
#include <stdint.h>
uint32_t sign_extend16(uint32_t val)
{
return (int32_t)(int16_t)val;
}
它是模拟器的一部分,所以有问题的操作不应该太奇怪。 通常情况下,我希望这只考虑较低的16位并将其符号扩展为32位。 显然,这是它多年来的行为。在x86_64上,GCC给了我这个结果:
0000000000000000 <sign_extend16>:
0: 0f bf c7 movswl %di,%eax
3: c3 retq
但是,根据我对该标准的理解,如果无法用带符号类型表示无符号的值,则无法将无符号转换为带符号。
编译器是否可以假设无符号值必须在[0, 32767]
范围内,因为任何其他值都是未定义的?在这种情况下,对int16_t
的强制转换和对int32_t
的强制转换将无效。在这种情况下,编译器将代码转换为简单的移动是否合法?
答案 0 :(得分:9)
两种整数类型之间的转换永远不会是未定义的行为。
但是一些整数转换是实现定义的。
在整数转换上,C表示:
(C99,6.3.1.3p3)“否则,新类型已签名且值无法在其中表示;结果是实现定义的,或者引发实现定义的信号。”
此案例中gcc
的内容记录在此处:
http://gcc.gnu.org/onlinedocs/gcc/Integers-implementation.html
“为了转换为宽度为N的类型,该值以2 ^ N的模数减少到该类型的范围内;没有信号被提升”
答案 1 :(得分:2)
如ouah所述,超出范围值的转换会给出实现定义的结果(或允许引发实现定义的信号)。
例如,对于实现而言,将超出范围的值转换为int16_t
仅保留该值的低15位并始终将符号位设置为是完全合法的。因此,它会将您的sign_extend16()
函数解释为return val & 0x7fff;
。
但是,实现无法解释您的函数,使其只返回val
不变 - 实现定义的转换为int16_t
必须在某处产生值int16_t
的范围,因此最终结果必须位于[0, 32767]
或[4294934528, 4294967295]
的某处。
另请注意,int32_t
投放完全是多余的。
两个不依赖于实现定义的转换的替代方案是(注意val
的参数类型的更改):
uint32_t se16(uint16_t val)
{
return -((uint32_t)val << 1 & 0x10000) | val;
}
uint32_t se16(uint16_t val)
{
return (val ^ (uint32_t)32768) - (uint32_t)32768;
}
...但不幸的是,gcc优化器似乎没有注意到这些只是低16位的符号扩展。
答案 2 :(得分:0)
我在评论中已经提到的两个版本:
#include <stdint.h>
uint32_t sign_extend16_a(uint32_t val)
{
return (uint32_t)(int16_t)(uint16_t)val;
}
uint32_t sign_extend16_b(uint32_t val)
{
union { uint16_t u; int16_t i; } ui;
ui.u = (uint16_t)val;
return (uint32_t)ui.i;
}
使用带有-O1
x86-64的gcc 4.5.3生成以下输出:
.globl sign_extend16_a
.def sign_extend16_a; .scl 2; .type 32; .endef
sign_extend16_a:
subq $8, %rsp
movswl %cx, %eax
addq $8, %rsp
ret
.globl sign_extend16_b
.def sign_extend16_b; .scl 2; .type 32; .endef
sign_extend16_b:
subq $8, %rsp
movswl %cx, %eax
addq $8, %rsp
ret
答案 3 :(得分:-1)
使用union:
uint32_t sign_extend16(uint32_t val){
union{
uint32_t a;
int32_t b;
int16_t c;
}o;
o.a=val;
o.b=o.c;
return o.a;
}