[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