[pypy-commit] pypy default: Performance tweaks to round(x, n) for the case n == 0
arigo
pypy.commits at gmail.com
Sun May 28 04:18:49 EDT 2017
Author: Armin Rigo <arigo at tunes.org>
Branch:
Changeset: r91427:3b55e802a373
Date: 2017-05-28 10:18 +0200
http://bitbucket.org/pypy/pypy/changeset/3b55e802a373/
Log: Performance tweaks to round(x, n) for the case n == 0
diff --git a/pypy/module/__builtin__/operation.py b/pypy/module/__builtin__/operation.py
--- a/pypy/module/__builtin__/operation.py
+++ b/pypy/module/__builtin__/operation.py
@@ -6,7 +6,7 @@
from pypy.interpreter.error import OperationError, oefmt
from pypy.interpreter.gateway import unwrap_spec, WrappedDefault
from rpython.rlib.runicode import UNICHR
-from rpython.rlib.rfloat import isnan, isinf, round_double
+from rpython.rlib.rfloat import isfinite, isinf, round_double, round_away
from rpython.rlib import rfloat
import __builtin__
@@ -134,23 +134,26 @@
ndigits = space.getindex_w(w_ndigits, None)
# nans, infinities and zeros round to themselves
- if number == 0 or isinf(number) or isnan(number):
- return space.newfloat(number)
-
- # Deal with extreme values for ndigits. For ndigits > NDIGITS_MAX, x
- # always rounds to itself. For ndigits < NDIGITS_MIN, x always
- # rounds to +-0.0.
- if ndigits > NDIGITS_MAX:
- return space.newfloat(number)
- elif ndigits < NDIGITS_MIN:
- # return 0.0, but with sign of x
- return space.newfloat(0.0 * number)
-
- # finite x, and ndigits is not unreasonably large
- z = round_double(number, ndigits)
- if isinf(z):
- raise oefmt(space.w_OverflowError,
- "rounded value too large to represent")
+ if not isfinite(number):
+ z = number
+ elif ndigits == 0: # common case
+ z = round_away(number)
+ # no need to check for an infinite 'z' here
+ else:
+ # Deal with extreme values for ndigits. For ndigits > NDIGITS_MAX, x
+ # always rounds to itself. For ndigits < NDIGITS_MIN, x always
+ # rounds to +-0.0.
+ if ndigits > NDIGITS_MAX:
+ z = number
+ elif ndigits < NDIGITS_MIN:
+ # return 0.0, but with sign of x
+ z = 0.0 * number
+ else:
+ # finite x, and ndigits is not unreasonably large
+ z = round_double(number, ndigits)
+ if isinf(z):
+ raise oefmt(space.w_OverflowError,
+ "rounded value too large to represent")
return space.newfloat(z)
# ____________________________________________________________
diff --git a/pypy/module/__builtin__/test/test_builtin.py b/pypy/module/__builtin__/test/test_builtin.py
--- a/pypy/module/__builtin__/test/test_builtin.py
+++ b/pypy/module/__builtin__/test/test_builtin.py
@@ -625,6 +625,9 @@
assert round(5e15) == 5e15
assert round(-(5e15-1)) == -(5e15-1)
assert round(-5e15) == -5e15
+ assert round(5e15/2) == 5e15/2
+ assert round((5e15+1)/2) == 5e15/2+1
+ assert round((5e15-1)/2) == 5e15/2
#
inf = 1e200 * 1e200
assert round(inf) == inf
@@ -636,6 +639,12 @@
#
assert round(562949953421312.5, 1) == 562949953421312.5
assert round(56294995342131.5, 3) == 56294995342131.5
+ #
+ for i in range(-10, 10):
+ expected = i if i < 0 else i + 1
+ assert round(i + 0.5) == round(i + 0.5, 0) == expected
+ x = i * 10 + 5
+ assert round(x, -1) == round(float(x), -1) == expected * 10
def test_vars_obscure_case(self):
class C_get_vars(object):
diff --git a/rpython/rlib/rfloat.py b/rpython/rlib/rfloat.py
--- a/rpython/rlib/rfloat.py
+++ b/rpython/rlib/rfloat.py
@@ -96,7 +96,20 @@
"""Round a float half away from zero.
Specify half_even=True to round half even instead.
+ The argument 'value' must be a finite number. This
+ function may return an infinite number in case of
+ overflow (only if ndigits is a very negative integer).
"""
+ if ndigits == 0:
+ # fast path for this common case
+ if half_even:
+ return round_half_even(value)
+ else:
+ return round_away(value)
+
+ if value == 0.0:
+ return 0.0
+
# The basic idea is very simple: convert and round the double to
# a decimal string using _Py_dg_dtoa, then convert that decimal
# string back to a double with _Py_dg_strtod. There's one minor
@@ -217,11 +230,34 @@
def round_away(x):
# round() from libm, which is not available on all platforms!
+ # This version rounds away from zero.
absx = abs(x)
- if absx - math.floor(absx) >= .5:
- r = math.ceil(absx)
+ r = math.floor(absx + 0.5)
+ if r - absx < 1.0:
+ return copysign(r, x)
else:
- r = math.floor(absx)
+ # 'absx' is just in the wrong range: its exponent is precisely
+ # the one for which all integers are representable but not any
+ # half-integer. It means that 'absx + 0.5' computes equal to
+ # 'absx + 1.0', which is not equal to 'absx'. So 'r - absx'
+ # computes equal to 1.0. In this situation, we can't return
+ # 'r' because 'absx' was already an integer but 'r' is the next
+ # integer! But just returning the original 'x' is fine.
+ return x
+
+def round_half_even(x):
+ absx = abs(x)
+ r = math.floor(absx + 0.5)
+ frac = r - absx
+ if frac >= 0.5:
+ # two rare cases: either 'absx' is precisely half-way between
+ # two integers (frac == 0.5); or we're in the same situation as
+ # described in round_away above (frac == 1.0).
+ if frac >= 1.0:
+ return x
+ # absx == n + 0.5 for a non-negative integer 'n'
+ # absx * 0.5 == n//2 + 0.25 or 0.75, which we round to nearest
+ r = math.floor(absx * 0.5 + 0.5) * 2.0
return copysign(r, x)
@not_rpython
diff --git a/rpython/rlib/test/test_rfloat.py b/rpython/rlib/test/test_rfloat.py
--- a/rpython/rlib/test/test_rfloat.py
+++ b/rpython/rlib/test/test_rfloat.py
@@ -28,7 +28,7 @@
def test_round_double():
def almost_equal(x, y):
- assert round(abs(x-y), 7) == 0
+ assert abs(x-y) < 1e-7
almost_equal(round_double(0.125, 2), 0.13)
almost_equal(round_double(0.375, 2), 0.38)
@@ -85,6 +85,13 @@
almost_equal(round_double(0.5e22, -22), 1e22)
almost_equal(round_double(1.5e22, -22), 2e22)
+ exact_integral = 5e15 + 1
+ assert round_double(exact_integral, 0) == exact_integral
+ assert round_double(exact_integral/2.0, 0) == 5e15/2.0 + 1.0
+ exact_integral = 5e15 - 1
+ assert round_double(exact_integral, 0) == exact_integral
+ assert round_double(exact_integral/2.0, 0) == 5e15/2.0
+
def test_round_half_even():
from rpython.rlib import rfloat
func = rfloat.round_double
@@ -92,6 +99,15 @@
assert func(2.5, 0, False) == 3.0
# 3.x behavior
assert func(2.5, 0, True) == 2.0
+ for i in range(-10, 10):
+ assert func(i + 0.5, 0, True) == i + (i & 1)
+ assert func(i * 10 + 5, -1, True) == (i + (i & 1)) * 10
+ exact_integral = 5e15 + 1
+ assert round_double(exact_integral, 0, True) == exact_integral
+ assert round_double(exact_integral/2.0, 0, True) == 5e15/2.0
+ exact_integral = 5e15 - 1
+ assert round_double(exact_integral, 0, True) == exact_integral
+ assert round_double(exact_integral/2.0, 0, True) == 5e15/2.0
def test_float_as_rbigint_ratio():
for f, ratio in [
More information about the pypy-commit
mailing list