float到有符号整数转换不匹配float到无符号整数

时间:2014-05-09 15:04:52

标签: c type-conversion

以下宏

#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

1 个答案:

答案 0 :(得分:3)

0.01,10/1000的数学结果,不能完全用二进制表示。对于中间浮点结果,一个编译器可能使用比类型(此处为float)所需的精度更高的精度。只要编译器将FLT_EVAL_METHOD定义为1或2,就可以在C99中以精确定义的方式允许这样做。一些非c99编译器也让中间结果具有过高的精度,这次没有明确定义何时舍入可以发生也可以不发生。

在二进制中,0.01的最接近表示可能在一个精度上高于0.01,在另一个精度上低于0.01。这将解释19与您的编译器和20与ideone。

如果编译器尊重C99关于允许超出精度的规则,则没有理由为ticksIntticksUint生成不同的值。但是,不遵守这些规则的编译器可以生成导致此情况发生的代码。

在命令行选项中添加-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”部分。