Question

The following macro

#define MS_TO_TICKS(ms) ( ( (float)(ms)/ MILLISECONDS_PER_SECOND) * clkRate() )

converts a value in milliseconds to the correct number of clock ticks. For some reason, if I store the result in a signed integer, i sometimes get a different value than if I store in an unsigned integer.

The code below illustrates the problem, and outputs the following:

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

This is running on a Core i7 processor, and compiled using gcc as follows:

ccpentium -g -mtune=pentium4 -march=pentium4 -nostdlib -fno-builtin -fno-defer-pop \
    -ansi  -Wall  -Werror -Wextra  -Wno-unused-parameter -MD -MP

I don't see the same behaviour using https://ideone.com/HaJVSJ

What is going on??

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==

As requested, assembly as output from compiler. I simplified the code slightly to:

int convertToSigned(int ms)
{
    return MS_TO_TICKS(ms);
}

unsigned int convertToUnsigned(int ms)
{
    return MS_TO_TICKS(ms);
}

Assembler (snippet) for 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

and for 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
Was it helpful?

Solution

0.01, the mathematical result of 10 / 1000, is not representable exactly in binary. It is possible that one compiler uses greater precision than required by the type (here, float) for intermediate floating-point results. This is allowed, in a precisely defined manner, in C99, as long as the compiler defines FLT_EVAL_METHOD to 1 or 2. Some non-c99 compilers also let intermediate results have excess precision, this time without a clear definition of when rounding can or cannot happen.

In binary, it is possible that the closest representation of 0.01 is higher than 0.01 at one precision, and lower at another. This would explain the 19 with your compiler and the 20 with ideone.

A compiler that would respect C99's rules for where excess precision is allowed would have no excuse to produce different values for ticksInt and ticksUint. However, a compiler that does not respect these rules can generate code that causes this to happen.

Adding -std=c99 to the commandline options makes GCC respect the letter of the C99 standard with respect to excess precision for floating-point expressions. Without this option, GCC's behavior with respect to excess precision (when excess precision there has to be, that is, when generating code for the 387 FPU) is very “casual”: results are kept in 80-bit registers and spilled to 64- or 32-bit slots on the stack at the compiler's whim, with no intervention of the programmer, causing unpredictable, erratic results.

This could completely explain what you are observing when you compile at home: for some unfathomable reason the value is converted to int directly from the 80-bit register but has gone from a 80-bit register to a 32-bit slot when the conversion to unsigned int takes place.

If this is the correct explanation, your solutions are:

  • do not generate 387 code: use GCC options -msse2 -mfpmath=sse;

  • use -std=c99 that, with a recent GCC, cause a sensible interpretation of what “excess precision” means, making floating-point code predictable;

  • do all computations in the long double type.

Please see the “This Said” part of this answer for additional details.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top