Uh oh!
There was an error while loading. Please reload this page.
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 into
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>#ifndefPHP_ROUND_HALF_UP#definePHP_ROUND_HALF_UP 0x01 /* Arithmetic rounding, up == away from zero */#endif#ifndefPHP_ROUND_HALF_DOWN#definePHP_ROUND_HALF_DOWN 0x02 /* Down == towards zero */#endif#ifndefPHP_ROUND_HALF_EVEN#definePHP_ROUND_HALF_EVEN 0x03 /* Banker's rounding */#endif#ifndefPHP_ROUND_HALF_ODD#definePHP_ROUND_HALF_ODD 0x04
#endifdoublephp_round_helper(doublevalue, intmode) {
doubletmp_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;
}
}
returntmp_value;
}
intmain(void) {
doubled;
doublevalues[] = { 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_ti=0; i<sizeof(values) / sizeof(values[0]); i++) {
doublevalue=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, |
bukka
commented
Sep 15, 2023
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) ClosesphpGH-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) ClosesGH-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.