Fix #12143: Improve precision of rounding for FP numbers#12159
Fix #12143: Improve precision of rounding for FP numbers#12159jorgsowa wants to merge 5 commits intophp:masterfrom
Conversation
| var_dump(round(0.49999999999999994, 0, PHP_ROUND_HALF_UP) == 0); | ||
| var_dump(round(0.49999999999999994, 0, PHP_ROUND_HALF_DOWN) == 0); | ||
| var_dump(round(0.49999999999999994, 0, PHP_ROUND_HALF_EVEN) == 0); | ||
| var_dump(round(0.49999999999999994, 0, PHP_ROUND_HALF_ODD) == 0); | ||
| var_dump(round(-0.49999999999999994, 0, PHP_ROUND_HALF_UP) == -0); | ||
| var_dump(round(-0.49999999999999994, 0, PHP_ROUND_HALF_DOWN) == 0); | ||
| var_dump(round(-0.49999999999999994, 0, PHP_ROUND_HALF_EVEN) == 0); | ||
| var_dump(round(-0.49999999999999994, 0, PHP_ROUND_HALF_ODD) == 0); |
There was a problem hiding this comment.
It might be better to dump the resulting number, rather than the result of the comparison.
There was a problem hiding this comment.
Changed it. And also added cases for the number 0.5000000000000004.
|
@jorgsowa |
|
For reference, here is a test program to easily check the behavior of #include <math.h>
#include <float.h>
#include <stdio.h>
#ifndef PHP_ROUND_HALF_UP
#define PHP_ROUND_HALF_UP 0x01 /* Arithmetic rounding, up == away from zero */
#endif
#ifndef PHP_ROUND_HALF_DOWN
#define PHP_ROUND_HALF_DOWN 0x02 /* Down == towards zero */
#endif
#ifndef PHP_ROUND_HALF_EVEN
#define PHP_ROUND_HALF_EVEN 0x03 /* Banker's rounding */
#endif
#ifndef PHP_ROUND_HALF_ODD
#define PHP_ROUND_HALF_ODD 0x04
#endif
double php_round_helper(double value, int mode) {
double tmp_value;
if (value >= 0.0) {
tmp_value = floor(value + 0.5);
if ((mode == PHP_ROUND_HALF_DOWN && value == (-0.5 + tmp_value)) ||
(mode == PHP_ROUND_HALF_EVEN && value == (0.5 + 2 * floor(tmp_value/2.0))) ||
(mode == PHP_ROUND_HALF_ODD && value == (0.5 + 2 * floor(tmp_value/2.0) - 1.0)))
{
tmp_value = tmp_value - 1.0;
}
} else {
tmp_value = ceil(value - 0.5);
if ((mode == PHP_ROUND_HALF_DOWN && value == (0.5 + tmp_value)) ||
(mode == PHP_ROUND_HALF_EVEN && value == (-0.5 + 2 * ceil(tmp_value/2.0))) ||
(mode == PHP_ROUND_HALF_ODD && value == (-0.5 + 2 * ceil(tmp_value/2.0) + 1.0)))
{
tmp_value = tmp_value + 1.0;
}
}
return tmp_value;
}
int
main(void) {
double d;
double values[] = { 0.5, 1.5, 2.5, 3.5, 32.5, 64.5, 1024.5, 2048.5, 4096.5, 4503599627370494.5, 4503599627370495.5, 4503599627370496.5, 9007199254740989.5, 9007199254740990.5, 9007199254740991.5, 9007199254740992.5 };
for (size_t i = 0; i < sizeof(values) / sizeof(values[0]); i++) {
double value = values[i];
d = nextafter(value, 0); printf("%.17f %.17f %.17f\n", d, php_round_helper(d, PHP_ROUND_HALF_UP), php_round_helper(d, PHP_ROUND_HALF_DOWN));
d = value; printf("%.17f %.17f %.17f\n", d, php_round_helper(d, PHP_ROUND_HALF_UP), php_round_helper(d, PHP_ROUND_HALF_DOWN));
d = nextafter(value, DBL_MAX); printf("%.17f %.17f %.17f\n", d, php_round_helper(d, PHP_ROUND_HALF_UP), php_round_helper(d, PHP_ROUND_HALF_DOWN));
printf("\n");
}
} |
| var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_UP)); | ||
| var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_DOWN)); | ||
| var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_EVEN)); | ||
| var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_ODD)); | ||
| var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_UP)); | ||
| var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_DOWN)); | ||
| var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_EVEN)); | ||
| var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_ODD)); |
There was a problem hiding this comment.
[IMO]
The internal representation of double precision around 0.5 in IEEE754 and the corresponding FP are as follows:
3fdfffffffffffff // 0.49999999999999994
3fe0000000000000 // 0.5
3fe0000000000001 // 0.50000000000000001
Therefore, considering the purpose of the test, it would be better to set the value as follows(There were some places where digits were missing):
| var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_UP)); | |
| var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_DOWN)); | |
| var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_EVEN)); | |
| var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_ODD)); | |
| var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_UP)); | |
| var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_DOWN)); | |
| var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_EVEN)); | |
| var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_ODD)); | |
| var_dump(round(0.5, 0, PHP_ROUND_HALF_UP)); | |
| var_dump(round(0.5, 0, PHP_ROUND_HALF_DOWN)); | |
| var_dump(round(0.5, 0, PHP_ROUND_HALF_EVEN)); | |
| var_dump(round(0.5, 0, PHP_ROUND_HALF_ODD)); | |
| var_dump(round(-0.5, 0, PHP_ROUND_HALF_UP)); | |
| var_dump(round(-0.5, 0, PHP_ROUND_HALF_DOWN)); | |
| var_dump(round(-0.5, 0, PHP_ROUND_HALF_EVEN)); | |
| var_dump(round(-0.5, 0, PHP_ROUND_HALF_ODD)); | |
| var_dump(round(0.50000000000000001, 0, PHP_ROUND_HALF_UP)); | |
| var_dump(round(0.50000000000000001, 0, PHP_ROUND_HALF_DOWN)); | |
| var_dump(round(0.50000000000000001, 0, PHP_ROUND_HALF_EVEN)); | |
| var_dump(round(0.50000000000000001, 0, PHP_ROUND_HALF_ODD)); | |
| var_dump(round(-0.50000000000000001, 0, PHP_ROUND_HALF_UP)); | |
| var_dump(round(-0.50000000000000001, 0, PHP_ROUND_HALF_DOWN)); | |
| var_dump(round(-0.50000000000000001, 0, PHP_ROUND_HALF_EVEN)); | |
| var_dump(round(-0.50000000000000001, 0, PHP_ROUND_HALF_ODD)); |
There was a problem hiding this comment.
We may also want to check 1.5 and 2.5 as well to confirm the operation of ODD and EVEN.
[FYI]
These internal representations and their corresponding FP are:
3ff7ffffffffffff // 1.4999999999999998
3ff8000000000000 // 1.5
3ff8000000000001 // 1.5000000000000002
4003ffffffffffff // 2.4999999999999996
4004000000000000 // 2.5
4004000000000001 // 2.5000000000000004
|
Doesn't this have the same effect as #12162 ? As I understand it the last part of condition is pretty much fallback to simple comparison check. Or will the previous mode specific checks have any sensible impact when used in pre-rounding? I assume that the additional check is only useful in pre-rounding, right? If so, it might be a slight waste to do it all the time. |
|
@bukka For example, it is wrong to round Double precision makes it impossible to distinguish between Also, |
|
Ah ok I see now. It makes sense |
This change makes the implementation much easier to understand, by explicitly handling the various cases. It fixes rounding for `0.49999999999999994`, because no loss of precision happens by adding / subtracing `0.5` before turning the result into an integral float. Instead the fractional parts are explicitly compared. see phpGH-12143 (this fixes one of the reported cases) Closes phpGH-12159 which was an alternative attempt to fix the rounding issue for `0.49999999999999994`
This change makes the implementation much easier to understand, by explicitly handling the various cases. It fixes rounding for `0.49999999999999994`, because no loss of precision happens by adding / subtracing `0.5` before turning the result into an integral float. Instead the fractional parts are explicitly compared. see GH-12143 (this fixes one of the reported cases) Closes GH-12159 which was an alternative attempt to fix the rounding issue for `0.49999999999999994`
This PR fixes #12143
When we calculate FP numbers with large precision and perform
floor(value + 0.5)orceil(value + 0.5)we can round the result to the full integer which lead to incorrect result. We need to check if the number is not changed due to FP representation.