-
Notifications
You must be signed in to change notification settings - Fork 7.9k
Fix #12143: Improve precision of rounding for FP numbers #12159
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might be better to dump the resulting number, rather than the result of the comparison.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.