Fix GH-12143: Optimize round

Fixed an error in the result due to "pre-rounding" of the round function.

"Pre-rounding" has been abolished and the method of comparing numbers has
been changed.

Closes GH-12268.
This commit is contained in:
Saki Takamachi 2024-02-03 22:23:02 +09:00
parent 0e93f03e65
commit 78970ef6b2
No known key found for this signature in database
GPG Key ID: E4A36F6D37931A8B
5 changed files with 207 additions and 122 deletions

1
NEWS
View File

@ -153,6 +153,7 @@ Standard:
the precision is not lost. (Marc Bennewitz) the precision is not lost. (Marc Bennewitz)
. Add support for 4 new rounding modes to the round() function. (Jorg Sowa) . Add support for 4 new rounding modes to the round() function. (Jorg Sowa)
. debug_zval_dump() now indicates whether an array is packed. (Max Semenik) . debug_zval_dump() now indicates whether an array is packed. (Max Semenik)
. Fix GH-12143 (Optimize round). (SakiTakamachi)
XML: XML:
. Added XML_OPTION_PARSE_HUGE parser option. (nielsdos) . Added XML_OPTION_PARSE_HUGE parser option. (nielsdos)

View File

@ -343,6 +343,11 @@ PDO_SQLITE:
RFC: https://wiki.php.net/rfc/new_rounding_modes_to_round_function RFC: https://wiki.php.net/rfc/new_rounding_modes_to_round_function
. debug_zval_dump() now indicates whether an array is packed. . debug_zval_dump() now indicates whether an array is packed.
. Fixed a bug caused by "pre-rounding" of the round() function. Previously, using
"pre-rounding" to treat a value like 0.285 (actually 0.28499999999999998) as a
decimal number and round it to 0.29. However, "pre-rounding" incorrectly rounds
certain numbers, so this fix removes "pre-rounding" and changes the way numbers
are compared, so that the values are correctly rounded as decimal numbers.
======================================== ========================================
6. New Functions 6. New Functions

View File

@ -27,53 +27,10 @@
#include <float.h> #include <float.h>
#include <math.h> #include <math.h>
#include <stdlib.h> #include <stdlib.h>
#include <fenv.h>
#include "basic_functions.h" #include "basic_functions.h"
/* {{{ php_intlog10abs
Returns floor(log10(fabs(val))), uses fast binary search */
static inline int php_intlog10abs(double value) {
value = fabs(value);
if (value < 1e-8 || value > 1e22) {
return (int)floor(log10(value));
} else {
/* Do a binary search with 5 steps */
int result = 15;
static const double values[] = {
1e-8, 1e-7, 1e-6, 1e-5, 1e-4, 1e-3, 1e-2, 1e-1, 1e0, 1e1, 1e2,
1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11, 1e12, 1e13,
1e14, 1e15, 1e16, 1e17, 1e18, 1e19, 1e20, 1e21, 1e22};
if (value < values[result]) {
result -= 8;
} else {
result += 8;
}
if (value < values[result]) {
result -= 4;
} else {
result += 4;
}
if (value < values[result]) {
result -= 2;
} else {
result += 2;
}
if (value < values[result]) {
result -= 1;
} else {
result += 1;
}
if (value < values[result]) {
result -= 1;
}
result -= 8;
return result;
}
}
/* }}} */
/* {{{ php_intpow10 /* {{{ php_intpow10
Returns pow(10.0, (double)power), uses fast lookup table for exact powers */ Returns pow(10.0, (double)power), uses fast lookup table for exact powers */
static inline double php_intpow10(int power) { static inline double php_intpow10(int power) {
@ -90,22 +47,30 @@ static inline double php_intpow10(int power) {
} }
/* }}} */ /* }}} */
/* {{{ php_round_helper static zend_always_inline double php_round_get_basic_edge_case(double integral, double exponent, int places)
Actually performs the rounding of a value to integer in a certain mode */ {
static inline double php_round_helper(double value, int mode) { return (places > 0)
double integral, fractional; ? fabs((integral + copysign(0.5, integral)) / exponent)
: fabs((integral + copysign(0.5, integral)) * exponent);
}
/* Split the input value into the integral and fractional part. static zend_always_inline double php_round_get_zero_edge_case(double integral, double exponent, int places)
* {
* Both parts will have the same sign as the input value. We take return (places > 0)
* the absolute value of the fractional part (which will not result ? fabs((integral) / exponent)
* in branches in the assembly) to make the following cases simpler. : fabs((integral) * exponent);
*/ }
fractional = fabs(modf(value, &integral));
/* {{{ php_round_helper
Actually performs the rounding of a value to integer in a certain mode */
static inline double php_round_helper(double integral, double value, double exponent, int places, int mode) {
double value_abs = fabs(value);
double edge_case;
switch (mode) { switch (mode) {
case PHP_ROUND_HALF_UP: case PHP_ROUND_HALF_UP:
if (fractional >= 0.5) { edge_case = php_round_get_basic_edge_case(integral, exponent, places);
if (value_abs >= edge_case) {
/* We must increase the magnitude of the integral part /* We must increase the magnitude of the integral part
* (rounding up / towards infinity). copysign(1.0, integral) * (rounding up / towards infinity). copysign(1.0, integral)
* will either result in 1.0 or -1.0 depending on the sign * will either result in 1.0 or -1.0 depending on the sign
@ -120,21 +85,24 @@ static inline double php_round_helper(double value, int mode) {
return integral; return integral;
case PHP_ROUND_HALF_DOWN: case PHP_ROUND_HALF_DOWN:
if (fractional > 0.5) { edge_case = php_round_get_basic_edge_case(integral, exponent, places);
if (value_abs > edge_case) {
return integral + copysign(1.0, integral); return integral + copysign(1.0, integral);
} }
return integral; return integral;
case PHP_ROUND_CEILING: case PHP_ROUND_CEILING:
if (value > 0.0 && fractional > 0.0) { edge_case = php_round_get_zero_edge_case(integral, exponent, places);
if (value > 0.0 && value_abs > edge_case) {
return integral + 1.0; return integral + 1.0;
} }
return integral; return integral;
case PHP_ROUND_FLOOR: case PHP_ROUND_FLOOR:
if (value < 0.0 && fractional > 0.0) { edge_case = php_round_get_zero_edge_case(integral, exponent, places);
if (value < 0.0 && value_abs > edge_case) {
return integral - 1.0; return integral - 1.0;
} }
@ -144,18 +112,18 @@ static inline double php_round_helper(double value, int mode) {
return integral; return integral;
case PHP_ROUND_AWAY_FROM_ZERO: case PHP_ROUND_AWAY_FROM_ZERO:
if (fractional > 0.0) { edge_case = php_round_get_zero_edge_case(integral, exponent, places);
if (value_abs > edge_case) {
return integral + copysign(1.0, integral); return integral + copysign(1.0, integral);
} }
return integral; return integral;
case PHP_ROUND_HALF_EVEN: case PHP_ROUND_HALF_EVEN:
if (fractional > 0.5) { edge_case = php_round_get_basic_edge_case(integral, exponent, places);
if (value_abs > edge_case) {
return integral + copysign(1.0, integral); return integral + copysign(1.0, integral);
} } else if (UNEXPECTED(value_abs == edge_case)) {
if (UNEXPECTED(fractional == 0.5)) {
bool even = !fmod(integral, 2.0); bool even = !fmod(integral, 2.0);
/* If the integral part is not even we can make it even /* If the integral part is not even we can make it even
@ -169,11 +137,10 @@ static inline double php_round_helper(double value, int mode) {
return integral; return integral;
case PHP_ROUND_HALF_ODD: case PHP_ROUND_HALF_ODD:
if (fractional > 0.5) { edge_case = php_round_get_basic_edge_case(integral, exponent, places);
if (value_abs > edge_case) {
return integral + copysign(1.0, integral); return integral + copysign(1.0, integral);
} } else if (UNEXPECTED(value_abs == edge_case)) {
if (UNEXPECTED(fractional == 0.5)) {
bool even = !fmod(integral, 2.0); bool even = !fmod(integral, 2.0);
if (even) { if (even) {
@ -196,63 +163,55 @@ static inline double php_round_helper(double value, int mode) {
* mode. For the specifics of the algorithm, see http://wiki.php.net/rfc/rounding * mode. For the specifics of the algorithm, see http://wiki.php.net/rfc/rounding
*/ */
PHPAPI double _php_math_round(double value, int places, int mode) { PHPAPI double _php_math_round(double value, int places, int mode) {
double f1, f2; double exponent;
double tmp_value; double tmp_value;
int precision_places; int cpu_round_mode;
if (!zend_finite(value) || value == 0.0) { if (!zend_finite(value) || value == 0.0) {
return value; return value;
} }
places = places < INT_MIN+1 ? INT_MIN+1 : places; places = places < INT_MIN+1 ? INT_MIN+1 : places;
precision_places = 14 - php_intlog10abs(value);
f1 = php_intpow10(abs(places)); exponent = php_intpow10(abs(places));
/* If the decimal precision guaranteed by FP arithmetic is higher than /**
the requested places BUT is small enough to make sure a non-zero value * When extracting the integer part, the result may be incorrect as a decimal
is returned, pre-round the result to the precision */ * number due to floating point errors.
if (precision_places > places && precision_places - 15 < places) { * e.g.
int64_t use_precision = precision_places < INT_MIN+1 ? INT_MIN+1 : precision_places; * 0.285 * 10000000000 => 2849999999.9999995
* floor(0.285 * 10000000000) => 2849999999
f2 = php_intpow10(abs((int)use_precision)); *
if (use_precision >= 0) { * Therefore, change the CPU rounding mode to away from 0 only from
tmp_value = value * f2; * fegetround to fesetround.
} else { * e.g.
tmp_value = value / f2; * 0.285 * 10000000000 => 2850000000.0
} * floor(0.285 * 10000000000) => 2850000000
/* preround the result (tmp_value will always be something * 1e14, */
thus never larger than 1e15 here) */ cpu_round_mode = fegetround();
tmp_value = php_round_helper(tmp_value, mode); if (value >= 0.0) {
fesetround(FE_UPWARD);
use_precision = places - precision_places; tmp_value = floor(places > 0 ? value * exponent : value / exponent);
use_precision = use_precision < INT_MIN+1 ? INT_MIN+1 : use_precision;
/* now correctly move the decimal point */
f2 = php_intpow10(abs((int)use_precision));
/* because places < precision_places */
tmp_value = tmp_value / f2;
} else { } else {
/* adjust the value */ fesetround(FE_DOWNWARD);
if (places >= 0) { tmp_value = ceil(places > 0 ? value * exponent : value / exponent);
tmp_value = value * f1; }
} else { fesetround(cpu_round_mode);
tmp_value = value / f1;
} /* This value is beyond our precision, so rounding it is pointless */
/* This value is beyond our precision, so rounding it is pointless */ if (fabs(tmp_value) >= 1e15) {
if (fabs(tmp_value) >= 1e15) { return value;
return value;
}
} }
/* round the temp value */ /* round the temp value */
tmp_value = php_round_helper(tmp_value, mode); tmp_value = php_round_helper(tmp_value, value, exponent, places, mode);
/* see if it makes sense to use simple division to round the value */ /* see if it makes sense to use simple division to round the value */
if (abs(places) < 23) { if (abs(places) < 23) {
if (places > 0) { if (places > 0) {
tmp_value = tmp_value / f1; tmp_value = tmp_value / exponent;
} else { } else {
tmp_value = tmp_value * f1; tmp_value = tmp_value * exponent;
} }
} else { } else {
/* Simple division can't be used since that will cause wrong results. /* Simple division can't be used since that will cause wrong results.
@ -272,7 +231,6 @@ PHPAPI double _php_math_round(double value, int places, int mode) {
tmp_value = value; tmp_value = value;
} }
} }
return tmp_value; return tmp_value;
} }
/* }}} */ /* }}} */

View File

@ -2,19 +2,65 @@
Bug #24142 (round() problems) Bug #24142 (round() problems)
--FILE-- --FILE--
<?php <?php
$v = 0.005; echo "round(0.005, 2)\n";
for ($i = 1; $i < 10; $i++) { var_dump(round(0.005, 2));
echo "round({$v}, 2) -> ".round($v, 2)."\n"; echo "\n";
$v += 0.01;
} echo "round(0.015, 2)\n";
var_dump(round(0.015, 2));
echo "\n";
echo "round(0.025, 2)\n";
var_dump(round(0.025, 2));
echo "\n";
echo "round(0.035, 2)\n";
var_dump(round(0.035, 2));
echo "\n";
echo "round(0.045, 2)\n";
var_dump(round(0.045, 2));
echo "\n";
echo "round(0.055, 2)\n";
var_dump(round(0.055, 2));
echo "\n";
echo "round(0.065, 2)\n";
var_dump(round(0.065, 2));
echo "\n";
echo "round(0.075, 2)\n";
var_dump(round(0.075, 2));
echo "\n";
echo "round(0.085, 2)\n";
var_dump(round(0.085, 2));
?> ?>
--EXPECT-- --EXPECT--
round(0.005, 2) -> 0.01 round(0.005, 2)
round(0.015, 2) -> 0.02 float(0.01)
round(0.025, 2) -> 0.03
round(0.035, 2) -> 0.04 round(0.015, 2)
round(0.045, 2) -> 0.05 float(0.02)
round(0.055, 2) -> 0.06
round(0.065, 2) -> 0.07 round(0.025, 2)
round(0.075, 2) -> 0.08 float(0.03)
round(0.085, 2) -> 0.09
round(0.035, 2)
float(0.04)
round(0.045, 2)
float(0.05)
round(0.055, 2)
float(0.06)
round(0.065, 2)
float(0.07)
round(0.075, 2)
float(0.08)
round(0.085, 2)
float(0.09)

View File

@ -0,0 +1,75 @@
--TEST--
Fix GH-12143: Optimize round
--FILE--
<?php
echo "HALF_UP\n";
var_dump(round(1.700000000000145, 13, PHP_ROUND_HALF_UP));
var_dump(round(-1.700000000000145, 13, PHP_ROUND_HALF_UP));
var_dump(round(123456789012344.5, -1, PHP_ROUND_HALF_UP));
var_dump(round(-123456789012344.5, -1, PHP_ROUND_HALF_UP));
echo "\n";
echo "HALF_DOWN\n";
var_dump(round(1.70000000000015, 13, PHP_ROUND_HALF_DOWN));
var_dump(round(-1.70000000000015, 13, PHP_ROUND_HALF_DOWN));
var_dump(round(123456789012345.0, -1, PHP_ROUND_HALF_DOWN));
var_dump(round(-123456789012345.0, -1, PHP_ROUND_HALF_DOWN));
var_dump(round(1.500000000000001, 0, PHP_ROUND_HALF_DOWN));
var_dump(round(-1.500000000000001, 0, PHP_ROUND_HALF_DOWN));
echo "\n";
echo "HALF_EVEN\n";
var_dump(round(1.70000000000025, 13, PHP_ROUND_HALF_EVEN));
var_dump(round(-1.70000000000025, 13, PHP_ROUND_HALF_EVEN));
var_dump(round(1.70000000000075, 13, PHP_ROUND_HALF_EVEN));
var_dump(round(-1.70000000000075, 13, PHP_ROUND_HALF_EVEN));
var_dump(round(12345678901234.5, 0, PHP_ROUND_HALF_EVEN));
var_dump(round(-12345678901234.5, 0, PHP_ROUND_HALF_EVEN));
var_dump(round(1.500000000000001, 0, PHP_ROUND_HALF_EVEN));
var_dump(round(-1.500000000000001, 0, PHP_ROUND_HALF_EVEN));
echo "\n";
echo "HALF_ODD\n";
var_dump(round(1.70000000000025, 13, PHP_ROUND_HALF_ODD));
var_dump(round(-1.70000000000025, 13, PHP_ROUND_HALF_ODD));
var_dump(round(1.70000000000075, 13, PHP_ROUND_HALF_ODD));
var_dump(round(-1.70000000000075, 13, PHP_ROUND_HALF_ODD));
var_dump(round(12345678901233.5, 0, PHP_ROUND_HALF_ODD));
var_dump(round(-12345678901233.5, 0, PHP_ROUND_HALF_ODD));
var_dump(round(1.500000000000001, 0, PHP_ROUND_HALF_ODD));
var_dump(round(-1.500000000000001, 0, PHP_ROUND_HALF_ODD));
?>
--EXPECT--
HALF_UP
float(1.7000000000001)
float(-1.7000000000001)
float(123456789012340)
float(-123456789012340)
HALF_DOWN
float(1.7000000000001)
float(-1.7000000000001)
float(123456789012340)
float(-123456789012340)
float(2)
float(-2)
HALF_EVEN
float(1.7000000000002)
float(-1.7000000000002)
float(1.7000000000008)
float(-1.7000000000008)
float(12345678901234)
float(-12345678901234)
float(2)
float(-2)
HALF_ODD
float(1.7000000000003)
float(-1.7000000000003)
float(1.7000000000007)
float(-1.7000000000007)
float(12345678901233)
float(-12345678901233)
float(2)
float(-2)