0

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
mjs
  • 2,837
  • 4
  • 28
  • 48
  • I'm more surprised that the signed int is 20 than that the unsigned int is 19. – Jonathan Leffler May 09 '14 at 15:25
  • I agree.... Although my comment says "Expected Value", what I really mean is "The Value I Wanted". I was _expecting_ 19 – mjs May 09 '14 at 15:27
  • GCC's option `-S` allows to see the generated assembly. Could you show us the relevant part of the generated assembly for your program? Also please add `printf("FLT_EVAL_METHOD:%d\n", (int)FLT_EVAL_METHOD);` somewhere. I believe `FLT_EVAL_METHOD` is defined in math.h. – Pascal Cuoq May 09 '14 at 15:32
  • Yes, but probably not till after the weekend now :-) – mjs May 09 '14 at 15:57
  • A workaround, for this case, is to move the division so that the macro is now `((ms) * ((float)clkRate())/MILLISECONDS_PER_SECOND)`, but that might just work out due to the clkRate chosen. In addition, using round(MS_TO_TICKS(ms)) also seems to do the correct thing. – mjs May 09 '14 at 16:19
  • @mjs I almost mentioned the possibility of using `round(…)` in my answer, but it must be pointed out that this does not suppress the strangeness, it only moves it to other values. With C's round-towards-zero behavior of conversion to an integer type, the issue is for values that can be above or below an integral value depending on subtle differences in rounding. If you add a call to `round(…)`, the problem is moved to values that can, depending on subtle differences in rounding, be above or below a number written XXX.5 in decimal. – Pascal Cuoq May 09 '14 at 16:47
  • FLT_EVAL_METHOD is not defined ( it is in float.h, but only defined for `__STDC_VERSION__ >= 199901L`. Will update Q with assembler as requested – mjs May 12 '14 at 09:27
  • That's interesting, your GCC generates assembly with plenty of 387 instructions (the `f...` instructions) and the SSE instruction `cvttss2si` in the middle of `convertToSigned`. – Pascal Cuoq May 12 '14 at 10:18
  • Why the sudden down vote 8 months after any activity? – mjs Feb 04 '15 at 22:01

1 Answers1

3

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.

Community
  • 1
  • 1
Pascal Cuoq
  • 79,187
  • 7
  • 161
  • 281
  • Thanks for the answer, but it doesn't explain why when converting to signed, it is rounding to 20, but with unsigned is rounding to 19. Or at least not clearly. – mjs May 09 '14 at 15:55
  • @mjs It does: if excess precision is used according to GCC's pre-C99 semantics, the behavior is **ERRATIC** and **UNPREDICTABLE**. “for some reason the value has gone from a 80-bit register to a 32-bit slot when one of the conversion functions is called but not the other.” – Pascal Cuoq May 09 '14 at 15:56
  • @mjs If this is the explanation, then it is never going to be “clear”, because the rounding off of excess precision happens **at the compiler's whim**, when it needs to free up a floating-point register, with no visible cause at the source level in the program. But you can see it happening in the assembly code if you wish to look for it. Please see David Monniaux's report, linked to in my other answer. And with a modern GCC, you can fix it with `-fexcess-precision=standard`, implied by `-std=c99`. – Pascal Cuoq May 09 '14 at 15:57
  • Ahh, when you say "one of the conversion functions" you mean either the conversion to signed or the conversion to unsigned? – mjs May 09 '14 at 16:03
  • Using `-msse2 -mfpmath=sse` works (although they round up to 20, which is perhaps still not what I was expecting). Using `-std=c99` doesn't change behaviour - I may be falling foul of the "with a recent GCC" qualifier. – mjs May 12 '14 at 09:45
  • Using using double cast instead of float cast in :`#define MS_TO_TICKS(ms) ( ( (double)(ms)/ MS_PER_SECOND) * clkRate() )` Also appears to work. – mjs May 12 '14 at 09:50
  • @mjs Why would you not expect 20? It is the mathematical result of 10 / 1000 * 2000. – Pascal Cuoq May 12 '14 at 10:20
  • Because I still had in my head "The answer was 19 and a bit, so it should truncate to 19". You are correct though, it should be 20 – mjs May 12 '14 at 10:33