4 ms·
This is great! There was a post on /r/softwaregore recently where someone showed a progress bar on Steam that said "57/100 achievements! Game 56% complete" or
by jacobmartin 4y ago
This is great!
There was a post on /r/softwaregore recently where someone showed a progress bar on Steam that said "57/100 achievements! Game 56% complete" or something like that. I snarkily commented something about naively using floor() on floating point and then moved on.
But then I thought that that may not have been the problem, fired up emacs and wrote a C program basically just saying
printf("%f\n", 57.0 / 100.0 * 100.0);
To my surprise this correctly gave 57.000000, but in python,
57 / 100 * 100
Gave 56.999... anybody know what was up here? Different algorithms for printing fp?
- lifthrasiir 4y agoThe calculated number is indeed 56.99999999999999289457264239899814128875732421875 which is not same to 57 (IEEE 754 binary32 or binary64 can represent this integer exactly). You can verify this with the following: printf("%f\n", 57.0 / 100.0 * 100.0 - 57.0); It just happens that `%f` in C defaults to 6 fractional digits.
- jacobmartin 4y agoThank you!
- creeble 4y agoCompiler optimization.
- lifthrasiir 4y agoCompilers are very cautious about floating point optimization because many real number identities do not hold in FP, so they won't do much unless instructed to do so. Even if they do a constant propagation they will generally calculate in binary, in decimal. (Exception: decimal literals in some languages like Go are untyped and can be exactly calculated before getting rounded at once.)
- kazinator 4y agoWrong guess; the algebraic optimization you might be thinking of is simply not allowed. The expression is likely subject to an optimization known as constant folding: the compiler calculates the value of the constant expression, and substitutes that value into the code. However, that constant-folding calculation has to produce the same result as what would happen at run-time: the 56.999999999999992895... approximation of 57. Constant-folding having to produce the same results as run-time creates a challenge in cross-compiling situations, when the host machine's math is different from the target machine's math. The compiler must emulate the target machine math.
- creeble 4y agoI guess I'm misunderstanding what `-frounding-math` for gcc does / does not do.
- OscarCunningham 4y agoIt's possible for games to have over 100 achievements, which could lead to it rounding to 0% even when they have an achievement, or to 100% when they still have one missing. People won't like this, so perhaps Steam put in some logic to adjust the numbers, and it is also affecting your case. EDIT: But I would have gone for ceiling(99*achievements/total), which does give 57% in this case.
- TheRealPomax 4y agoAnd what optimization did you tell the C compiler to use?
- TheRealPomax 4y ago(why did that get a downvote? It's C: optimizer flags are stupidly important, and half the things you think are defaults are actually undefined behaviour...)
- jacobmartin 4y agoI didn't downvote you. (In fact, I don't have access to the downvote button lol, but I wouldn't have used it here in any case.) But a comment above yours already discussed this possibility, and there were replies as to why this is not the problem here. C and Python are giving the same fp result just printed to different precisions. Ftr I was using the defaults, whatever those may be: gcc fp.c -o fp
- kazinator 4y agoThe f in %f doesn't mean "floating-point"; it means "fixed digits": printing the value in the style -dddd.dddd. If you don't specify how many digits after the decimal point, it defaults to six. If 56.999999999999 is rounded to 6 digits after the decimal point, you get 57.000000.
- jacobmartin 4y agoYes, thank you for this. I just ran it with the floor() function actually called and it did give 56.000000. I didn't think to check the precision. Rookie mistake!
- kazinator 4y agoThus, if you have a minute, try "%.20f" instead of %f. :) Or, how about this: #include <stdio.h> int main(void) { for (int prec = 0; prec < 25; prec++) { printf("%.*f\n", prec, 57.0 / 100.0 * 100.0); } return 0; } Output: 57 57.0 57.00 57.000 57.0000 57.00000 57.000000 57.0000000 57.00000000 57.000000000 57.0000000000 57.00000000000 57.000000000000 57.0000000000000 56.99999999999999 56.999999999999993 56.9999999999999929 56.99999999999999289 56.999999999999992895 56.9999999999999928946 56.99999999999999289457 56.999999999999992894573 56.9999999999999928945726 56.99999999999999289457264 56.999999999999992894572642 The 64 bit double will store 15 decimal digits reliably. That is to say, if you have a decimal figure with 15 significant digits, which is in range of the type (and not mapping to a denormal value close to zero and whatnot), all 15 digits are representable and can be recovered. In the reverse direction, you need about 17 decimal digits in order to capture an 64 bit double as decimal text such that the exact value can be recovered from the decimal text. Thus in the above loop's output, once we are past 17 digits (including the 56 before the decimal point), we are no longer seeing any new data, just a continuation of the fraction. And, notice how the last value that is still 57.000.... is exactly 15 digits wide. The next row is 16 digits, and that's where we now have 56.9999.... but 16 digits isn't quite enough to capture the value. I believe the next row gets us that: the ...99929. If we use that as a constant, any digits after that make no difference. Programming languages which, by default, print floating-point values to 15 digits will show the nice result .1 + .2 = .3. This is what I did in TXR Lisp. 1> *print-flo-precision* 15 2> (+ .1 .2) 0.3 3> (set *print-flo-precision* 16) 16 4> (+ .1 .2) 0.3 5> (set *print-flo-precision* 17) 17 6> (+ .1 .2) 0.30000000000000004 We can see there is no value difference in digits beyond 17: 7> (eq 0.30000000000000004 0.300000000000000049) t 8> (eq 0.30000000000000004 0.300000000000000040) t To get different value (different floating-point bit pattern), we need a difference in the 17th digit. And not just a single increment: 9> (eq 0.30000000000000004 0.30000000000000003) t 10> (eq 0.30000000000000004 0.30000000000000002) t The last digit being 3 and 2 is still mapping to the same value. When we make it 1, we start getting a different float: 11> (eq 0.30000000000000004 0.30000000000000001) nil