【问题标题】:float to signed integer conversion does not match float to unsigned integer浮点到有符号整数的转换不匹配浮点到无符号整数
【发布时间】:2014-05-09 15:04:51
【问题描述】:

下面的宏

#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 的汇编程序 (sn-p):

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

【问题讨论】:

  • 带符号的 int 是 20 比 unsigned int 是 19 更让我惊讶。
  • 我同意....虽然我的评论说“预期价值”,但我真正的意思是“我想要的价值”。我期待 19
  • GCC 的选项-S 允许查看生成的程序集。您能否向我们展示为您的程序生成的程序集的相关部分?另外请在某处添加printf("FLT_EVAL_METHOD:%d\n", (int)FLT_EVAL_METHOD);。我相信FLT_EVAL_METHOD 是在 math.h 中定义的。
  • 是的,但可能要等到周末之后 :-)
  • 在这种情况下,一种解决方法是移动除法,使宏现在为((ms) * ((float)clkRate())/MILLISECONDS_PER_SECOND),但由于选择了 clkRate,这可能会奏效。此外,使用 round(MS_TO_TICKS(ms)) 似乎也做了正确的事情。

标签: c type-conversion


【解决方案1】:

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

在二进制中,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”部分了解更多详情。

【讨论】:

  • 感谢您的回答,但它没有解释为什么在转换为有符号时,它四舍五入为 20,而无符号则四舍五入为 19。或者至少不清楚。
  • @mjs 确实如此:如果根据 GCC 的 C99 之前的语义使用了过高的精度,则行为是 ERRATICUNPREDICTABLE。 “由于某种原因,当调用其中一个转换函数而不是另一个时,该值已从 80 位寄存器变为 32 位插槽。”
  • @mjs 如果这是解释,那么它永远不会“清楚”,因为过度精度的舍入发生在编译器的突发奇想,当它需要时释放浮点寄存器,在程序的源代码级别没有明显的原因。但是,如果您想查找它,您可以在汇编代码中看到它。请参阅 David Monniaux 的报告,链接到我的其他答案。使用现代 GCC,您可以使用 -fexcess-precision=standard 修复它,由 -std=c99 暗示。
  • 啊,当您说“转换函数之一”时,您的意思是转换为有符号还是转换为无符号?
  • 使用-msse2 -mfpmath=sse 有效(尽管它们最多为20,这可能仍然不是我所期望的)。使用 -std=c99 不会改变行为 - 我可能会违反“使用最近的 GCC”限定词。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-11-01
  • 1970-01-01
  • 1970-01-01
  • 2018-09-23
  • 2021-11-17
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多