mirror of
https://github.com/php/php-src.git
synced 2024-09-21 09:57:23 +00:00
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:
parent
0e93f03e65
commit
78970ef6b2
1
NEWS
1
NEWS
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
/* }}} */
|
/* }}} */
|
||||||
|
@ -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)
|
||||||
|
75
ext/standard/tests/math/round_gh12143_optimize_round.phpt
Normal file
75
ext/standard/tests/math/round_gh12143_optimize_round.phpt
Normal 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)
|
Loading…
Reference in New Issue
Block a user