Skip to content

Commit 78970ef

Browse files
committed
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.
1 parent 0e93f03 commit 78970ef

File tree

5 files changed

+207
-122
lines changed

5 files changed

+207
-122
lines changed

NEWS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ Standard:
153153
the precision is not lost. (Marc Bennewitz)
154154
. Add support for 4 new rounding modes to the round() function. (Jorg Sowa)
155155
. debug_zval_dump() now indicates whether an array is packed. (Max Semenik)
156+
. Fix GH-12143 (Optimize round). (SakiTakamachi)
156157

157158
XML:
158159
. Added XML_OPTION_PARSE_HUGE parser option. (nielsdos)

UPGRADING

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,11 @@ PDO_SQLITE:
343343

344344
RFC: https://wiki.php.net/rfc/new_rounding_modes_to_round_function
345345
. debug_zval_dump() now indicates whether an array is packed.
346+
. Fixed a bug caused by "pre-rounding" of the round() function. Previously, using
347+
"pre-rounding" to treat a value like 0.285 (actually 0.28499999999999998) as a
348+
decimal number and round it to 0.29. However, "pre-rounding" incorrectly rounds
349+
certain numbers, so this fix removes "pre-rounding" and changes the way numbers
350+
are compared, so that the values ​​are correctly rounded as decimal numbers.
346351

347352
========================================
348353
6. New Functions

ext/standard/math.c

Lines changed: 66 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -27,53 +27,10 @@
2727
#include <float.h>
2828
#include <math.h>
2929
#include <stdlib.h>
30+
#include <fenv.h>
3031

3132
#include "basic_functions.h"
3233

33-
/* {{{ php_intlog10abs
34-
Returns floor(log10(fabs(val))), uses fast binary search */
35-
static inline int php_intlog10abs(double value) {
36-
value = fabs(value);
37-
38-
if (value < 1e-8 || value > 1e22) {
39-
return (int)floor(log10(value));
40-
} else {
41-
/* Do a binary search with 5 steps */
42-
int result = 15;
43-
static const double values[] = {
44-
1e-8, 1e-7, 1e-6, 1e-5, 1e-4, 1e-3, 1e-2, 1e-1, 1e0, 1e1, 1e2,
45-
1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11, 1e12, 1e13,
46-
1e14, 1e15, 1e16, 1e17, 1e18, 1e19, 1e20, 1e21, 1e22};
47-
48-
if (value < values[result]) {
49-
result -= 8;
50-
} else {
51-
result += 8;
52-
}
53-
if (value < values[result]) {
54-
result -= 4;
55-
} else {
56-
result += 4;
57-
}
58-
if (value < values[result]) {
59-
result -= 2;
60-
} else {
61-
result += 2;
62-
}
63-
if (value < values[result]) {
64-
result -= 1;
65-
} else {
66-
result += 1;
67-
}
68-
if (value < values[result]) {
69-
result -= 1;
70-
}
71-
result -= 8;
72-
return result;
73-
}
74-
}
75-
/* }}} */
76-
7734
/* {{{ php_intpow10
7835
Returns pow(10.0, (double)power), uses fast lookup table for exact powers */
7936
static inline double php_intpow10(int power) {
@@ -90,22 +47,30 @@ static inline double php_intpow10(int power) {
9047
}
9148
/* }}} */
9249

93-
/* {{{ php_round_helper
94-
Actually performs the rounding of a value to integer in a certain mode */
95-
static inline double php_round_helper(double value, int mode) {
96-
double integral, fractional;
50+
static zend_always_inline double php_round_get_basic_edge_case(double integral, double exponent, int places)
51+
{
52+
return (places > 0)
53+
? fabs((integral + copysign(0.5, integral)) / exponent)
54+
: fabs((integral + copysign(0.5, integral)) * exponent);
55+
}
9756

98-
/* Split the input value into the integral and fractional part.
99-
*
100-
* Both parts will have the same sign as the input value. We take
101-
* the absolute value of the fractional part (which will not result
102-
* in branches in the assembly) to make the following cases simpler.
103-
*/
104-
fractional = fabs(modf(value, &integral));
57+
static zend_always_inline double php_round_get_zero_edge_case(double integral, double exponent, int places)
58+
{
59+
return (places > 0)
60+
? fabs((integral) / exponent)
61+
: fabs((integral) * exponent);
62+
}
63+
64+
/* {{{ php_round_helper
65+
Actually performs the rounding of a value to integer in a certain mode */
66+
static inline double php_round_helper(double integral, double value, double exponent, int places, int mode) {
67+
double value_abs = fabs(value);
68+
double edge_case;
10569

10670
switch (mode) {
10771
case PHP_ROUND_HALF_UP:
108-
if (fractional >= 0.5) {
72+
edge_case = php_round_get_basic_edge_case(integral, exponent, places);
73+
if (value_abs >= edge_case) {
10974
/* We must increase the magnitude of the integral part
11075
* (rounding up / towards infinity). copysign(1.0, integral)
11176
* 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) {
12085
return integral;
12186

12287
case PHP_ROUND_HALF_DOWN:
123-
if (fractional > 0.5) {
88+
edge_case = php_round_get_basic_edge_case(integral, exponent, places);
89+
if (value_abs > edge_case) {
12490
return integral + copysign(1.0, integral);
12591
}
12692

12793
return integral;
12894

12995
case PHP_ROUND_CEILING:
130-
if (value > 0.0 && fractional > 0.0) {
96+
edge_case = php_round_get_zero_edge_case(integral, exponent, places);
97+
if (value > 0.0 && value_abs > edge_case) {
13198
return integral + 1.0;
13299
}
133100

134101
return integral;
135102

136103
case PHP_ROUND_FLOOR:
137-
if (value < 0.0 && fractional > 0.0) {
104+
edge_case = php_round_get_zero_edge_case(integral, exponent, places);
105+
if (value < 0.0 && value_abs > edge_case) {
138106
return integral - 1.0;
139107
}
140108

@@ -144,18 +112,18 @@ static inline double php_round_helper(double value, int mode) {
144112
return integral;
145113

146114
case PHP_ROUND_AWAY_FROM_ZERO:
147-
if (fractional > 0.0) {
115+
edge_case = php_round_get_zero_edge_case(integral, exponent, places);
116+
if (value_abs > edge_case) {
148117
return integral + copysign(1.0, integral);
149118
}
150119

151120
return integral;
152121

153122
case PHP_ROUND_HALF_EVEN:
154-
if (fractional > 0.5) {
123+
edge_case = php_round_get_basic_edge_case(integral, exponent, places);
124+
if (value_abs > edge_case) {
155125
return integral + copysign(1.0, integral);
156-
}
157-
158-
if (UNEXPECTED(fractional == 0.5)) {
126+
} else if (UNEXPECTED(value_abs == edge_case)) {
159127
bool even = !fmod(integral, 2.0);
160128

161129
/* 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) {
169137
return integral;
170138

171139
case PHP_ROUND_HALF_ODD:
172-
if (fractional > 0.5) {
140+
edge_case = php_round_get_basic_edge_case(integral, exponent, places);
141+
if (value_abs > edge_case) {
173142
return integral + copysign(1.0, integral);
174-
}
175-
176-
if (UNEXPECTED(fractional == 0.5)) {
143+
} else if (UNEXPECTED(value_abs == edge_case)) {
177144
bool even = !fmod(integral, 2.0);
178145

179146
if (even) {
@@ -196,63 +163,55 @@ static inline double php_round_helper(double value, int mode) {
196163
* mode. For the specifics of the algorithm, see http://wiki.php.net/rfc/rounding
197164
*/
198165
PHPAPI double _php_math_round(double value, int places, int mode) {
199-
double f1, f2;
166+
double exponent;
200167
double tmp_value;
201-
int precision_places;
168+
int cpu_round_mode;
202169

203170
if (!zend_finite(value) || value == 0.0) {
204171
return value;
205172
}
206173

207174
places = places < INT_MIN+1 ? INT_MIN+1 : places;
208-
precision_places = 14 - php_intlog10abs(value);
209175

210-
f1 = php_intpow10(abs(places));
176+
exponent = php_intpow10(abs(places));
211177

212-
/* If the decimal precision guaranteed by FP arithmetic is higher than
213-
the requested places BUT is small enough to make sure a non-zero value
214-
is returned, pre-round the result to the precision */
215-
if (precision_places > places && precision_places - 15 < places) {
216-
int64_t use_precision = precision_places < INT_MIN+1 ? INT_MIN+1 : precision_places;
217-
218-
f2 = php_intpow10(abs((int)use_precision));
219-
if (use_precision >= 0) {
220-
tmp_value = value * f2;
221-
} else {
222-
tmp_value = value / f2;
223-
}
224-
/* preround the result (tmp_value will always be something * 1e14,
225-
thus never larger than 1e15 here) */
226-
tmp_value = php_round_helper(tmp_value, mode);
227-
228-
use_precision = places - precision_places;
229-
use_precision = use_precision < INT_MIN+1 ? INT_MIN+1 : use_precision;
230-
/* now correctly move the decimal point */
231-
f2 = php_intpow10(abs((int)use_precision));
232-
/* because places < precision_places */
233-
tmp_value = tmp_value / f2;
178+
/**
179+
* When extracting the integer part, the result may be incorrect as a decimal
180+
* number due to floating point errors.
181+
* e.g.
182+
* 0.285 * 10000000000 => 2849999999.9999995
183+
* floor(0.285 * 10000000000) => 2849999999
184+
*
185+
* Therefore, change the CPU rounding mode to away from 0 only from
186+
* fegetround to fesetround.
187+
* e.g.
188+
* 0.285 * 10000000000 => 2850000000.0
189+
* floor(0.285 * 10000000000) => 2850000000
190+
*/
191+
cpu_round_mode = fegetround();
192+
if (value >= 0.0) {
193+
fesetround(FE_UPWARD);
194+
tmp_value = floor(places > 0 ? value * exponent : value / exponent);
234195
} else {
235-
/* adjust the value */
236-
if (places >= 0) {
237-
tmp_value = value * f1;
238-
} else {
239-
tmp_value = value / f1;
240-
}
241-
/* This value is beyond our precision, so rounding it is pointless */
242-
if (fabs(tmp_value) >= 1e15) {
243-
return value;
244-
}
196+
fesetround(FE_DOWNWARD);
197+
tmp_value = ceil(places > 0 ? value * exponent : value / exponent);
198+
}
199+
fesetround(cpu_round_mode);
200+
201+
/* This value is beyond our precision, so rounding it is pointless */
202+
if (fabs(tmp_value) >= 1e15) {
203+
return value;
245204
}
246205

247206
/* round the temp value */
248-
tmp_value = php_round_helper(tmp_value, mode);
207+
tmp_value = php_round_helper(tmp_value, value, exponent, places, mode);
249208

250209
/* see if it makes sense to use simple division to round the value */
251210
if (abs(places) < 23) {
252211
if (places > 0) {
253-
tmp_value = tmp_value / f1;
212+
tmp_value = tmp_value / exponent;
254213
} else {
255-
tmp_value = tmp_value * f1;
214+
tmp_value = tmp_value * exponent;
256215
}
257216
} else {
258217
/* 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) {
272231
tmp_value = value;
273232
}
274233
}
275-
276234
return tmp_value;
277235
}
278236
/* }}} */

ext/standard/tests/math/bug24142.phpt

Lines changed: 60 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,65 @@
22
Bug #24142 (round() problems)
33
--FILE--
44
<?php
5-
$v = 0.005;
6-
for ($i = 1; $i < 10; $i++) {
7-
echo "round({$v}, 2) -> ".round($v, 2)."\n";
8-
$v += 0.01;
9-
}
5+
echo "round(0.005, 2)\n";
6+
var_dump(round(0.005, 2));
7+
echo "\n";
8+
9+
echo "round(0.015, 2)\n";
10+
var_dump(round(0.015, 2));
11+
echo "\n";
12+
13+
echo "round(0.025, 2)\n";
14+
var_dump(round(0.025, 2));
15+
echo "\n";
16+
17+
echo "round(0.035, 2)\n";
18+
var_dump(round(0.035, 2));
19+
echo "\n";
20+
21+
echo "round(0.045, 2)\n";
22+
var_dump(round(0.045, 2));
23+
echo "\n";
24+
25+
echo "round(0.055, 2)\n";
26+
var_dump(round(0.055, 2));
27+
echo "\n";
28+
29+
echo "round(0.065, 2)\n";
30+
var_dump(round(0.065, 2));
31+
echo "\n";
32+
33+
echo "round(0.075, 2)\n";
34+
var_dump(round(0.075, 2));
35+
echo "\n";
36+
37+
echo "round(0.085, 2)\n";
38+
var_dump(round(0.085, 2));
1039
?>
1140
--EXPECT--
12-
round(0.005, 2) -> 0.01
13-
round(0.015, 2) -> 0.02
14-
round(0.025, 2) -> 0.03
15-
round(0.035, 2) -> 0.04
16-
round(0.045, 2) -> 0.05
17-
round(0.055, 2) -> 0.06
18-
round(0.065, 2) -> 0.07
19-
round(0.075, 2) -> 0.08
20-
round(0.085, 2) -> 0.09
41+
round(0.005, 2)
42+
float(0.01)
43+
44+
round(0.015, 2)
45+
float(0.02)
46+
47+
round(0.025, 2)
48+
float(0.03)
49+
50+
round(0.035, 2)
51+
float(0.04)
52+
53+
round(0.045, 2)
54+
float(0.05)
55+
56+
round(0.055, 2)
57+
float(0.06)
58+
59+
round(0.065, 2)
60+
float(0.07)
61+
62+
round(0.075, 2)
63+
float(0.08)
64+
65+
round(0.085, 2)
66+
float(0.09)

0 commit comments

Comments
 (0)