From d80f7bdc8de2a32f1d31b85ca2aaf95df91b49ff Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Wed, 30 Oct 2019 11:31:52 +0200 Subject: [PATCH 1/3] bpo-38639: Optimize floor(), ceil() and trunc() for floats. --- .../2019-10-30-11-31-47.bpo-38639.9-vKtO.rst | 2 ++ Modules/mathmodule.c | 32 ++++++++++++------- 2 files changed, 22 insertions(+), 12 deletions(-) create mode 100644 Misc/NEWS.d/next/Core and Builtins/2019-10-30-11-31-47.bpo-38639.9-vKtO.rst diff --git a/Misc/NEWS.d/next/Core and Builtins/2019-10-30-11-31-47.bpo-38639.9-vKtO.rst b/Misc/NEWS.d/next/Core and Builtins/2019-10-30-11-31-47.bpo-38639.9-vKtO.rst new file mode 100644 index 00000000000000..4432afde64d6e1 --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2019-10-30-11-31-47.bpo-38639.9-vKtO.rst @@ -0,0 +1,2 @@ +Optimized :func:`math.floor()`, :func:`math.ceil()` and :func:`math.trunc()` +for floats. diff --git a/Modules/mathmodule.c b/Modules/mathmodule.c index e1b46ec384a373..f4aa6381bcbe06 100644 --- a/Modules/mathmodule.c +++ b/Modules/mathmodule.c @@ -1114,15 +1114,17 @@ math_ceil(PyObject *module, PyObject *number) _Py_IDENTIFIER(__ceil__); PyObject *method, *result; - method = _PyObject_LookupSpecial(number, &PyId___ceil__); - if (method == NULL) { + if (!PyFloat_CheckExact(number)) { + method = _PyObject_LookupSpecial(number, &PyId___ceil__); + if (method != NULL) { + result = _PyObject_CallNoArg(method); + Py_DECREF(method); + return result; + } if (PyErr_Occurred()) return NULL; - return math_1_to_int(number, ceil, 0); } - result = _PyObject_CallNoArg(method); - Py_DECREF(method); - return result; + return math_1_to_int(number, ceil, 0); } FUNC2(copysign, copysign, @@ -1172,15 +1174,17 @@ math_floor(PyObject *module, PyObject *number) _Py_IDENTIFIER(__floor__); PyObject *method, *result; - method = _PyObject_LookupSpecial(number, &PyId___floor__); - if (method == NULL) { + if (!PyFloat_CheckExact(number)) { + method = _PyObject_LookupSpecial(number, &PyId___floor__); + if (method != NULL) { + result = _PyObject_CallNoArg(method); + Py_DECREF(method); + return result; + } if (PyErr_Occurred()) return NULL; - return math_1_to_int(number, floor, 0); } - result = _PyObject_CallNoArg(method); - Py_DECREF(method); - return result; + return math_1_to_int(number, floor, 0); } FUNC1A(gamma, m_tgamma, @@ -2061,6 +2065,10 @@ math_trunc(PyObject *module, PyObject *x) _Py_IDENTIFIER(__trunc__); PyObject *trunc, *result; + if (PyFloat_CheckExact(x)) { + return PyFloat_Type.tp_as_number->nb_int(x); + } + if (Py_TYPE(x)->tp_dict == NULL) { if (PyType_Ready(Py_TYPE(x)) < 0) return NULL; From 265271aaaa44443682e633eb0be3950bbe3d20cb Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Wed, 30 Oct 2019 22:23:58 +0200 Subject: [PATCH 2/3] Get rid of math_1_to_int. --- Modules/mathmodule.c | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/Modules/mathmodule.c b/Modules/mathmodule.c index f4aa6381bcbe06..eaaeedbef3823b 100644 --- a/Modules/mathmodule.c +++ b/Modules/mathmodule.c @@ -1013,12 +1013,6 @@ math_1(PyObject *arg, double (*func) (double), int can_overflow) return math_1_to_whatever(arg, func, PyFloat_FromDouble, can_overflow); } -static PyObject * -math_1_to_int(PyObject *arg, double (*func) (double), int can_overflow) -{ - return math_1_to_whatever(arg, func, PyLong_FromDouble, can_overflow); -} - static PyObject * math_2(PyObject *const *args, Py_ssize_t nargs, double (*func) (double, double), const char *funcname) @@ -1112,19 +1106,22 @@ math_ceil(PyObject *module, PyObject *number) /*[clinic end generated code: output=6c3b8a78bc201c67 input=2725352806399cab]*/ { _Py_IDENTIFIER(__ceil__); - PyObject *method, *result; if (!PyFloat_CheckExact(number)) { - method = _PyObject_LookupSpecial(number, &PyId___ceil__); + PyObject *method = _PyObject_LookupSpecial(number, &PyId___ceil__); if (method != NULL) { - result = _PyObject_CallNoArg(method); + PyObject *result = _PyObject_CallNoArg(method); Py_DECREF(method); return result; } if (PyErr_Occurred()) return NULL; } - return math_1_to_int(number, ceil, 0); + double x = PyFloat_AsDouble(number); + if (x == -1.0 && PyErr_Occurred()) + return NULL; + + return PyLong_FromDouble(ceil(x)); } FUNC2(copysign, copysign, @@ -1172,19 +1169,22 @@ math_floor(PyObject *module, PyObject *number) /*[clinic end generated code: output=c6a65c4884884b8a input=63af6b5d7ebcc3d6]*/ { _Py_IDENTIFIER(__floor__); - PyObject *method, *result; if (!PyFloat_CheckExact(number)) { - method = _PyObject_LookupSpecial(number, &PyId___floor__); + PyObject *method = _PyObject_LookupSpecial(number, &PyId___floor__); if (method != NULL) { - result = _PyObject_CallNoArg(method); + PyObject *result = _PyObject_CallNoArg(method); Py_DECREF(method); return result; } if (PyErr_Occurred()) return NULL; } - return math_1_to_int(number, floor, 0); + double x = PyFloat_AsDouble(number); + if (x == -1.0 && PyErr_Occurred()) + return NULL; + + return PyLong_FromDouble(floor(x)); } FUNC1A(gamma, m_tgamma, From 864845af6ff17a5e43a0844f0079410eba6d6a4a Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Thu, 7 Nov 2019 22:23:06 +0200 Subject: [PATCH 3/3] Add tests. --- Lib/test/test_math.py | 61 ++++++++++++++++++++++++++++--------------- 1 file changed, 40 insertions(+), 21 deletions(-) diff --git a/Lib/test/test_math.py b/Lib/test/test_math.py index c237bc1942e655..f832246dda3227 100644 --- a/Lib/test/test_math.py +++ b/Lib/test/test_math.py @@ -240,6 +240,13 @@ def result_check(expected, got, ulp_tol=5, abs_tol=0.0): else: return None +class FloatLike: + def __init__(self, value): + self.value = value + + def __float__(self): + return self.value + class IntSubclass(int): pass @@ -397,12 +404,14 @@ def testAtan2(self): def testCeil(self): self.assertRaises(TypeError, math.ceil) self.assertEqual(int, type(math.ceil(0.5))) - self.ftest('ceil(0.5)', math.ceil(0.5), 1) - self.ftest('ceil(1.0)', math.ceil(1.0), 1) - self.ftest('ceil(1.5)', math.ceil(1.5), 2) - self.ftest('ceil(-0.5)', math.ceil(-0.5), 0) - self.ftest('ceil(-1.0)', math.ceil(-1.0), -1) - self.ftest('ceil(-1.5)', math.ceil(-1.5), -1) + self.assertEqual(math.ceil(0.5), 1) + self.assertEqual(math.ceil(1.0), 1) + self.assertEqual(math.ceil(1.5), 2) + self.assertEqual(math.ceil(-0.5), 0) + self.assertEqual(math.ceil(-1.0), -1) + self.assertEqual(math.ceil(-1.5), -1) + self.assertEqual(math.ceil(0.0), 0) + self.assertEqual(math.ceil(-0.0), 0) #self.assertEqual(math.ceil(INF), INF) #self.assertEqual(math.ceil(NINF), NINF) #self.assertTrue(math.isnan(math.ceil(NAN))) @@ -410,9 +419,14 @@ def testCeil(self): class TestCeil: def __ceil__(self): return 42 + class FloatCeil(float): + def __ceil__(self): + return 42 class TestNoCeil: pass - self.ftest('ceil(TestCeil())', math.ceil(TestCeil()), 42) + self.assertEqual(math.ceil(TestCeil()), 42) + self.assertEqual(math.ceil(FloatCeil()), 42) + self.assertEqual(math.ceil(FloatLike(42.5)), 43) self.assertRaises(TypeError, math.ceil, TestNoCeil()) t = TestNoCeil() @@ -536,16 +550,12 @@ def testFactorialHugeInputs(self): def testFloor(self): self.assertRaises(TypeError, math.floor) self.assertEqual(int, type(math.floor(0.5))) - self.ftest('floor(0.5)', math.floor(0.5), 0) - self.ftest('floor(1.0)', math.floor(1.0), 1) - self.ftest('floor(1.5)', math.floor(1.5), 1) - self.ftest('floor(-0.5)', math.floor(-0.5), -1) - self.ftest('floor(-1.0)', math.floor(-1.0), -1) - self.ftest('floor(-1.5)', math.floor(-1.5), -2) - # pow() relies on floor() to check for integers - # This fails on some platforms - so check it here - self.ftest('floor(1.23e167)', math.floor(1.23e167), 1.23e167) - self.ftest('floor(-1.23e167)', math.floor(-1.23e167), -1.23e167) + self.assertEqual(math.floor(0.5), 0) + self.assertEqual(math.floor(1.0), 1) + self.assertEqual(math.floor(1.5), 1) + self.assertEqual(math.floor(-0.5), -1) + self.assertEqual(math.floor(-1.0), -1) + self.assertEqual(math.floor(-1.5), -2) #self.assertEqual(math.ceil(INF), INF) #self.assertEqual(math.ceil(NINF), NINF) #self.assertTrue(math.isnan(math.floor(NAN))) @@ -553,9 +563,14 @@ def testFloor(self): class TestFloor: def __floor__(self): return 42 + class FloatFloor(float): + def __floor__(self): + return 42 class TestNoFloor: pass - self.ftest('floor(TestFloor())', math.floor(TestFloor()), 42) + self.assertEqual(math.floor(TestFloor()), 42) + self.assertEqual(math.floor(FloatFloor()), 42) + self.assertEqual(math.floor(FloatLike(41.9)), 41) self.assertRaises(TypeError, math.floor, TestNoFloor()) t = TestNoFloor() @@ -1448,17 +1463,21 @@ def test_trunc(self): self.assertEqual(math.trunc(-0.999999), -0) self.assertEqual(math.trunc(-100.999), -100) - class TestTrunc(object): + class TestTrunc: def __trunc__(self): return 23 - - class TestNoTrunc(object): + class FloatTrunc(float): + def __trunc__(self): + return 23 + class TestNoTrunc: pass self.assertEqual(math.trunc(TestTrunc()), 23) + self.assertEqual(math.trunc(FloatTrunc()), 23) self.assertRaises(TypeError, math.trunc) self.assertRaises(TypeError, math.trunc, 1, 2) + self.assertRaises(TypeError, math.trunc, FloatLike(23.5)) self.assertRaises(TypeError, math.trunc, TestNoTrunc()) def testIsfinite(self):