以下宏
#define MS_TO_TICKS(ms) ( ( (float)(ms)/ MILLISECONDS_PER_SECOND) * clkRate() )
将以毫秒为单位的值转换为正确的时钟周期数。出于某种原因,如果我将结果存储在有符号整数中,我有时会获得与存储无符号整数时不同的值。
下面的代码说明了问题,并输出以下内容:
Milliseconds: 7
Expected val: 14
Signed Int : 14 //OK
Unsigned Int: 14 //Still OK
Floating Pnt: 14.0000000000000
Double Precn: 14.0000004321337
Direct Macro: 14.0000004321337
Milliseconds: 10
Expected val: 20
Signed Int : 20 //Expected value, looks like it rounded up
Unsigned Int: 19 //Rounded Down? What?????
Floating Pnt: 20.0000000000000
Double Precn: 19.9999995529652
Direct Macro: 19.9999995529652
这是在Core i7处理器上运行,并使用gcc编译如下:
ccpentium -g -mtune=pentium4 -march=pentium4 -nostdlib -fno-builtin -fno-defer-pop \
-ansi -Wall -Werror -Wextra -Wno-unused-parameter -MD -MP
我没有看到使用https://ideone.com/HaJVSJ
的相同行为发生了什么事?
int clkRate()
{
return 2000;
}
const int MILLISECONDS_PER_SECOND = 1000;
#define MS_TO_TICKS(ms) ( ( (float)(ms)/ MILLISECONDS_PER_SECOND) * clkRate() )
void convertAndPrint(int ms)
{
int ticksInt;
unsigned ticksUint;
double ticksDbl;
float ticksFlt;
ticksInt = MS_TO_TICKS(ms);
ticksUint= MS_TO_TICKS(ms);
ticksFlt = MS_TO_TICKS(ms);
ticksDbl = MS_TO_TICKS(ms);
printf("Milliseconds: %i\n", ms);
printf("Expected val: %i\n",ms*2);
printf("Signed Int : %2i\n"
"Unsigned Int: %2u\n"
"Floating Pnt: %.13f\n"
"Double Precn: %.13f\n"
"Direct Macro: %.13f\n",
ticksInt,ticksUint,ticksFlt, ticksDbl, MS_TO_TICKS(ms));
}
void weirdConversionDemo(void)
{
convertAndPrint(7);
convertAndPrint(10);
}
== EDIT ==
根据要求,汇编为编译器的输出。我将代码略微简化为:
int convertToSigned(int ms)
{
return MS_TO_TICKS(ms);
}
unsigned int convertToUnsigned(int ms)
{
return MS_TO_TICKS(ms);
}
convertToSigned的汇编程序(代码段):
fildl 8(%ebp)
movl MS_PER_SECOND, %eax
pushl %eax
fildl (%esp)
leal 4(%esp), %esp
fdivrp %st, %st(1)
fstps -4(%ebp)
call clkRate
pushl %eax
fildl (%esp)
leal 4(%esp), %esp
fmuls -4(%ebp)
fstps -8(%ebp)
movss -8(%ebp), %xmm0
cvttss2si %xmm0, %eax
和convertToUnsigned
fildl 8(%ebp)
movl MS_PER_SECOND, %eax
pushl %eax
fildl (%esp)
leal 4(%esp), %esp
fdivrp %st, %st(1)
fstps -20(%ebp)
call clkRate
pushl %eax
fildl (%esp)
leal 4(%esp), %esp
fmuls -20(%ebp)
fnstcw -2(%ebp)
movzwl -2(%ebp), %eax
movb $12, %ah
movw %ax, -4(%ebp)
fldcw -4(%ebp)
fistpll -16(%ebp)
fldcw -2(%ebp)
movl -16(%ebp), %eax
movl -12(%ebp), %edx
答案 0 :(得分:3)
0.01,10/1000的数学结果,不能完全用二进制表示。对于中间浮点结果,一个编译器可能使用比类型(此处为float
)所需的精度更高的精度。只要编译器将FLT_EVAL_METHOD
定义为1或2,就可以在C99中以精确定义的方式允许这样做。一些非c99编译器也让中间结果具有过高的精度,这次没有明确定义何时舍入可以发生也可以不发生。
在二进制中,0.01的最接近表示可能在一个精度上高于0.01,在另一个精度上低于0.01。这将解释19与您的编译器和20与ideone。
如果编译器尊重C99关于允许超出精度的规则,则没有理由为ticksInt
和ticksUint
生成不同的值。但是,不遵守这些规则的编译器可以生成导致此情况发生的代码。
在命令行选项中添加-std=c99
使得GCC尊重C99标准中关于浮点表达式的超精度的字母。如果没有这个选项,GCC在超精度方面的行为(当需要过多的精度时,也就是生成387 FPU的代码时)非常“随意”:结果保存在80位寄存器中并溢出到64-编译器突发奇想时堆栈上的32位插槽,没有程序员介入,导致不可预测的,不稳定的结果。
这可以完全解释您在家编译时所观察到的内容:由于某些不可理喻的原因,该值直接从80位寄存器转换为int
,但已从80位寄存器转换为32转换为unsigned int
时的位槽。
如果这是正确的解释,您的解决方案是:
不生成387代码:使用GCC选项-msse2 -mfpmath=sse
;
使用-std=c99
,使用最近的GCC,对“超额精度”的含义做出明智的解释,使浮点代码可预测;
以long double
类型进行所有计算。
有关其他详细信息,请参阅this answer的“This Said”部分。