php.net |  support |  documentation |  report a bug |  advanced search |  search howto |  statistics |  random bug |  login
Doc Bug #42294 round will not use PHP_ROUND_FUZZ on 64bit CPUs
Submitted: 2007-08-14 10:46 UTC Modified: 2010-01-07 12:47 UTC
Votes:20
Avg. Score:4.3 ± 0.7
Reproduced:20 of 20 (100.0%)
Same Version:6 (30.0%)
Same OS:7 (35.0%)
From: oliver at teqneers dot de Assigned: kalle (profile)
Status: Closed Package: Documentation problem
PHP Version: 5CVS-2008-10-24 OS: *
Private report: No CVE-ID: None
 [2007-08-14 10:46 UTC] oliver at teqneers dot de
Description:
------------
Because of the nature of floats to be not exact enough in binary system, PHP uses a kind of fuzzy login to round more precisely turning on PHP_ROUND_FUZZ in some cases.

The problem is the "configure" check. The check does not work for AMD nor INTEL 64bit CPUs.

The number to test against is not sufficient. So maybe it is possible to add or change it???

Reproduce code:
---------------
#include <math.h>
  /* keep this out-of-line to prevent use of gcc inline floor() */
  double somefn(double n) {
    return floor(n*pow(10,2) + 0.5);
  }
  int main() {
    return somefn(0.045)/10.0 != 0.5;
  }

this will return 1 on a 32bit but 0 on a 64bit engine.

Expected result:
----------------
it should also fail on a 64bit and return 1. The following code would actually work on 64bit, because it will test 0.45 as well as 0.285:


#include <math.h>
  /* keep this out-of-line to prevent use of gcc inline floor() */
  double somefn(double n) {
    return floor(n*pow(10,2) + 0.5);
  }
  int main() {
    return (somefn(0.285)/100.0 != 0.29) || (somefn(0.045) / 10.0 != 0.5);
  }

Actual result:
--------------
Instead of using the fuzzy round, PHP just takes 0.5 and adds/substracts it from the given float (math.c)

Patches

Pull Requests

History

AllCommentsChangesGit/SVN commitsRelated reports
 [2007-08-15 08:47 UTC] jani@php.net
Can you please provide a short PHP code example which fails because of this..?
 [2007-08-15 11:25 UTC] oliver at teqneers dot de
this is an example of the comment block of the round documentation (http://de.php.net/manual/en/function.round.php):

<?php

printf("0.285 - %s <br> ",round(0.285,2));      
// incorrect result 0.28

printf("1.285 - %s <br> ",round(1.285,2));      
// correct result 1.29

printf("1.255 - %s <br><br>",round(1.255,2));   
// incorrect result 1.25

?>
 [2007-08-16 10:29 UTC] jani@php.net
Those results you get are NOT incorrect. If you round using 2 decimals, of course that's what you get.
 [2007-08-16 10:30 UTC] jani@php.net
Output for me on a 32bit system:
0.285 - 0.28
1.285 - 1.29
1.255 - 1.25

 [2007-08-16 11:34 UTC] oliver at teqneers dot de
you might use a windows machine? there the fuzzy seems to be always turned off (I don't know why though???). 

math.c:
-------
#ifndef PHP_ROUND_FUZZ
# ifndef PHP_WIN32
#  define PHP_ROUND_FUZZ 0.50000000001
# else
#  define PHP_ROUND_FUZZ 0.5
# endif
#endif




But I have to admit, that I forgot to switch to the php5 versions on our 32bit server. So on all our PHP4 versions with 32bit, round seems to work as mathematically expected. Sorry for that. 

BUT still the result could be correct with PHP_ROUND_FUZZ on, like it did with PHP4. I compiled the latest PHP5 (5.2.4RC1) and changed the configure check to be more precise. After that the round was working much better, than it did before. My current result with the patched configure script is:
0.285 - 0.29
1.285 - 1.29 
1.255 - 1.26

I mean, why is the FUZZ implemented and not used???
 [2007-08-16 11:39 UTC] jani@php.net
Assigned to Ilia who implemented that PHP_ROUND_FUZZ.
 [2007-08-16 12:53 UTC] chris_se at gmx dot net
I just read this bug report and wanted to add a few things. Rounding floating point numbers is anything but trivial.

The core issue is that certain numbers which are representable with only a finite amount of digits in the decimal system are not necessarily representable with a finite amount of digits in the binary system. The number 0.285 in this bug report is an example for that: In the binary system, its representation is periodic - just as 1/3 can only be displayed as a periodic number (0.333333333...) in the decimal system.

Since a floating point number only supports a finite number of digits, the period is "cut off" and therefore the number 0.285 stored as a float is not exactly 0.285 but slightly smaller, you can try it yourself:
2
<?php
$f = 0.285;
printf("%.20f\n", $f);
?>
0.28499999999999997558

(The exact representation may vary depending on how percise the floating point unit in your processor is.)

Another number 1.285 mentioned in this thread also has the same problem:

<?php
$f = 1.285;
printf("%.20f\n", $f);
?>
1.28499999999999992006

Now, the traditional rounding method in the decimal system is to take the lower number for the digits 0, 1, 2, 3 and 4 and the higher number for the digits 5, 6, 7, 8 and 9. So 1.4 becomes 1 and 1.5 becomes 2 if rounded to zero digits precision.

The problem is that if the internal representation of the floating point number is 0.2849...something instead of 0.285, the rounding algorithm will incorrectly assume the last digit is a 4 and not a 5 and then return the lower number instead of the higher one.

Now one may ask why does 1.285 work and 0.285 doesn't if both are not representable using finite digits in the binary system? This is due to the way the rounding algorithm works: It first multiplies the numbers by 10 to the power of the places of precision (with 2 places precision, it multiplies them with 100) and then it rounds to the next integer. Now, if you have a look at the representation of 1.285 * 100 and 0.285 * 100, you will get:

<?php
$f = 1.285 * 100;
printf("%.20f\n", $f);
$f = 0.285 * 100;
printf("%.20f\n", $f);
?>
128.50000000000000000000
28.49999999999999644729

Of course, one might argue that 28.5 is infact representable as a floating point number - sure, but that does not matter the computer always calculates with floating point numbers - so in the case of 128.5 the computer actually makes two errors due to decreased precision: The first is not being able to correctly represent 1.285 and the second is to accidentally compensate that error due to lack of precision. With 0.285, only the first error happens and so the result is incorrect.

So that's the reason why round() does not always work as expected. Now there are two possibilities to solve this:

1) Don't give a shit about the error and simply calculate as before. This is what the Linux implementation of the C99 function round(3) does (and probably the C99 standard itself, but I don't know since I haven't looked into it).

2) Try to correct the error: This is what the PHP_ROUND_FUZZ code is fore. A bit of background: A round() function is available in C only from C99 onwards - to ensure compability, PHP does rounding manually using floor/ceil. In order to keep this post short, I'll just look at positive numbers. So the current implementation of PHP's round() as found in ext/standard/math.c does the following:

#define PHP_ROUND_WITH_FUZZ(val, places) {			\
	double tmp_val=val, f = pow(10.0, (double) places);	\
	tmp_val *= f;					\
	if (tmp_val >= 0.0) {				\
		tmp_val = floor(tmp_val + PHP_ROUND_FUZZ);	\
	} else {					\
		tmp_val = ceil(tmp_val - PHP_ROUND_FUZZ);	\
	}						\
	tmp_val /= f;					\
	val = !zend_isnan(tmp_val) ? tmp_val : val;	\
}							\

Let's assume for a moment that PHP_ROUND_FUZZ is 0.5, then the code is obvious: 0.5 is added to the number and then floor() is called. That will produce the identical result for positive numbers as round() does.

Now, a possible correction for the rounding error is setting PHP_ROUND_FUZZ to 0.50000000001 - the last digit 1 does just enough to make round() work as expected.

Obviously, this code has one minor drawback: If one wants to round 0.49999999999 to 0 places precision, the "corrected" function will incorrectly return 1 instead of 0 here. On the other hand, this tiny fuzz will correct the VAST majority of other cases where round() fails.

So, now - what does PHP do? It has a configure check that tries to figure out if the fuzz is to be applied - by testing rounding with a sample number (0.045 to two places). What was the problem of the original reporter? 0.045 was a number where the "second error correcting the first one" kicks in if it is rounded on a 64 bit system with a higher internal precision of the FPU (probably 128 bit instead of 80 or something like that). His idea was to additionally test another number - 0.285 to two digits. And well, somehow because of the #ifndef in math.c, it always disables the fuzz for Windows systems.

My problem with this approach PHP currently uses is the following: The test does not make sense! There are ALWAYS cases where rounding will not work when using floating point numbers (either with a correction or without). So - if the test was written correctly - it would ALWAYS fail - there is NOT THE SLIGHTEST POSSIBILITY that a system implementing floating point semantics the same way PHP expects it to will always deliver correct rounding results! And the fuzz would ALWAYS be used! On the other hand, the fuzz is apparently ALWAYS disabled on Windows system.

Now, if I want to write a PHP script, I want it to be portable. I don't want to have to think about how much FPU precision somebody has or if it's a 32bit or 64bit system or if it's a Windows, Linux or Mac OS X box. So in my eyes, there are only three sensible ways of solving this problem:

1) Do it as the Linux C99 implementation does: Don't care about the
   errors and simple use the simple formula
   floor(value * 10^(places) + 0.5) / 10^(places)
   (and for negative values accordingly)
   But then DOCUMENT this behaviour so that anybody who wants to
   use the round function knows exactly what expects him.
2) Always correct the error with the fuzz and don't care about the
   extremely rare cases where the fuzz actually is contraproductive.
   But also then: DOCUMENT this behaviour.
3) Make the behaviour configurable either in the php.ini or with an
   additional optional parameter to round() or something else.

Staying with the current approach (some systems have it activated, some don't) will always cause portability issues of the round function. Don't do that. Choose the way you want the function to behave make sure it does so on EVERY operating system and architecture.
 [2007-10-30 22:20 UTC] jani@php.net
Previous comment has good points. I'm leaning towards the C99 approach..but that's for Ilia to decide. :)
 [2008-10-24 16:17 UTC] jani@php.net
Nearly a year since last test, still fails. Ilia? No comments for above comments?
 [2008-10-29 20:18 UTC] iliaa@php.net
This bug has been fixed in CVS.

Snapshots of the sources are packaged every three hours; this change
will be in the next snapshot. You can grab the snapshot at
http://snaps.php.net/.
 
Thank you for the report, and for helping us make PHP better.


 [2008-10-29 20:56 UTC] bjori@php.net
See http://news.php.net/php.cvs/53768 for developers notes.
 [2008-11-17 17:04 UTC] jani@php.net
reclassify in right category

 [2010-01-07 12:47 UTC] svn@php.net
Automatic comment from SVN on behalf of kalle
Revision: http://svn.php.net/viewvc/?view=revision&revision=293215
Log: * Fixed bug #42294 (round will not use PHP_ROUND_FUZZ on 64bit CPUs)
* Removed another PHP3 reference
 [2010-01-07 12:47 UTC] kalle@php.net
This bug has been fixed in the documentation's XML sources. Since the
online and downloadable versions of the documentation need some time
to get updated, we would like to ask you to be a bit patient.

Thank you for the report, and for helping us make our documentation better.


 [2020-02-07 06:09 UTC] phpdocbot@php.net
Automatic comment on behalf of kalle
Revision: http://git.php.net/?p=doc/en.git;a=commit;h=103ed7161312a30fcb2514e420c1f7a70b4c8ba9
Log: * Fixed bug #42294 (round will not use PHP_ROUND_FUZZ on 64bit CPUs) * Removed another PHP3 reference
 
PHP Copyright © 2001-2024 The PHP Group
All rights reserved.
Last updated: Sat Nov 23 21:01:28 2024 UTC