Skip to content
Closed
1 change: 1 addition & 0 deletions Include/internal/pycore_floatobject.h
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ extern PyObject* _Py_string_to_number_with_underscores(

extern double _Py_parse_inf_or_nan(const char *p, char **endptr);

PyAPI_FUNC(PyObject *) _PyFloat_From64Bits(int64_t);

#ifdef __cplusplus
}
Expand Down
62 changes: 33 additions & 29 deletions Include/internal/pycore_uop_ids.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions Include/internal/pycore_uop_metadata.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

75 changes: 58 additions & 17 deletions Lib/test/test_capi/test_opt.py
Original file line number Diff line number Diff line change
Expand Up @@ -794,48 +794,44 @@ def testfunc(n):

def test_float_add_constant_propagation(self):
def testfunc(n):
a = 1.0
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a limitation of loop-based traces. We should start projecting traces from function entries as well, but I'll leave that for Mark or someone else to do in the future.

for _ in range(n):
a = 1.0
a = a + 0.25
a = a + 0.25
a = a + 0.25
a = a + 0.25
return a

res, ex = self._run_with_optimizer(testfunc, 32)
self.assertAlmostEqual(res, 33.0)
self.assertAlmostEqual(res, 2.0)
self.assertIsNotNone(ex)
uops = get_opnames(ex)
guard_both_float_count = [opname for opname in iter_opnames(ex) if opname == "_GUARD_BOTH_FLOAT"]
self.assertLessEqual(len(guard_both_float_count), 1)
# TODO gh-115506: this assertion may change after propagating constants.
# We'll also need to verify that propagation actually occurs.
self.assertIn("_BINARY_OP_ADD_FLOAT", uops)
self.assertNotIn("_BINARY_OP_ADD_FLOAT", uops)
self.assertIn("_LOAD_FLOAT", uops)

def test_float_subtract_constant_propagation(self):
def testfunc(n):
a = 1.0
for _ in range(n):
a = 1.0
a = a - 0.25
a = a - 0.25
a = a - 0.25
a = a - 0.25
return a

res, ex = self._run_with_optimizer(testfunc, 32)
self.assertAlmostEqual(res, -31.0)
self.assertAlmostEqual(res, 0.0)
self.assertIsNotNone(ex)
uops = get_opnames(ex)
guard_both_float_count = [opname for opname in iter_opnames(ex) if opname == "_GUARD_BOTH_FLOAT"]
self.assertLessEqual(len(guard_both_float_count), 1)
# TODO gh-115506: this assertion may change after propagating constants.
# We'll also need to verify that propagation actually occurs.
self.assertIn("_BINARY_OP_SUBTRACT_FLOAT", uops)
self.assertNotIn("_BINARY_OP_SUBTRACT_FLOAT", uops)
self.assertIn("_LOAD_FLOAT", uops)

def test_float_multiply_constant_propagation(self):
def testfunc(n):
a = 1.0
for _ in range(n):
a = 1.0
a = a * 1.0
a = a * 1.0
a = a * 1.0
Expand All @@ -848,9 +844,22 @@ def testfunc(n):
uops = get_opnames(ex)
guard_both_float_count = [opname for opname in iter_opnames(ex) if opname == "_GUARD_BOTH_FLOAT"]
self.assertLessEqual(len(guard_both_float_count), 1)
# TODO gh-115506: this assertion may change after propagating constants.
# We'll also need to verify that propagation actually occurs.
self.assertIn("_BINARY_OP_MULTIPLY_FLOAT", uops)
self.assertNotIn("_BINARY_OP_MULTIPLY_FLOAT", uops)
self.assertIn("_LOAD_FLOAT", uops)

def test_int_add_constant_propagation_peepholer_advanced(self):
def testfunc(n):
for _ in range(n):
a = 1
a = (a + a) + (a + a + (a + a))
return a

res, ex = self._run_with_optimizer(testfunc, 32)
self.assertAlmostEqual(res, 6)
self.assertIsNotNone(ex)
uops = get_opnames(ex)
self.assertNotIn("_BINARY_OP_ADD_INT", uops)
self.assertIn("_LOAD_INT", uops)

def test_add_unicode_propagation(self):
def testfunc(n):
Expand Down Expand Up @@ -941,7 +950,8 @@ def testfunc(n):
self.assertIsNotNone(ex)
uops = get_opnames(ex)
self.assertNotIn("_GUARD_BOTH_INT", uops)
self.assertIn("_BINARY_OP_ADD_INT", uops)
# Constant folded
self.assertIn("_LOAD_INT", uops)
# Try again, but between the runs, set the global to a float.
# This should result in no executor the second time.
ns = {}
Expand Down Expand Up @@ -1245,5 +1255,36 @@ def testfunc(n):
self.assertEqual(res, 32 * 32)
self.assertIsNone(ex)

def test_int_constant_propagation(self):
def testfunc(n):
for _ in range(n):
a = 1
x = a + a - a * a
return x

res, ex = self._run_with_optimizer(testfunc, 32)
self.assertTrue(res)
self.assertIsNotNone(ex)
uops = get_opnames(ex)
self.assertNotIn("_BINARY_OP_ADD_INT", uops)
self.assertNotIn("_BINARY_OP_MULTIPLY_INT", uops)
self.assertNotIn("_BINARY_OP_SUBTRACT_INT", uops)

def test_no_bigint_constant_propagation(self):
# We don't want to hold strong references in the trace.
def testfunc(n):
for _ in range(n):
a = 100000000000000000000000000000000000000
x = a + a - a * a
return x

res, ex = self._run_with_optimizer(testfunc, 32)
self.assertTrue(res)
self.assertIsNotNone(ex)
uops = get_opnames(ex)
self.assertIn("_BINARY_OP_ADD_INT", uops)
self.assertIn("_BINARY_OP_MULTIPLY_INT", uops)
self.assertIn("_BINARY_OP_SUBTRACT_INT", uops)

if __name__ == "__main__":
unittest.main()
9 changes: 9 additions & 0 deletions Objects/floatobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -2614,3 +2614,12 @@ PyFloat_Unpack8(const char *data, int le)
return x;
}
}

PyObject*
_PyFloat_From64Bits(int64_t val)
{
assert(sizeof(double) == sizeof(int64_t));
double dst;
memcpy(&dst, &val, sizeof(int64_t));
return PyFloat_FromDouble(dst);
}
24 changes: 24 additions & 0 deletions Python/bytecodes.c
Original file line number Diff line number Diff line change
Expand Up @@ -4127,6 +4127,30 @@ dummy_func(
null = NULL;
}

tier2 pure op(_LOAD_INT, (cached/4 -- value)) {
value = PyLong_FromLong((int64_t)cached);
ERROR_IF(value == NULL, error);
}

tier2 pure op(_POP_TWO_LOAD_INT, (cached/4, pop1, pop2 -- value)) {
Py_DECREF(pop1);
Py_DECREF(pop2);
value = PyLong_FromLong((int64_t)cached);
ERROR_IF(value == NULL, error);
}

tier2 pure op(_LOAD_FLOAT, (cached/4: int64_t -- value)) {
value = _PyFloat_From64Bits((int64_t)cached);
ERROR_IF(value == NULL, error);
}

tier2 pure op(_POP_TWO_LOAD_FLOAT, (cached/4: int64_t, pop1, pop2 -- value)) {
Py_DECREF(pop1);
Py_DECREF(pop2);
value = _PyFloat_From64Bits((int64_t)cached);
ERROR_IF(value == NULL, error);
}

tier2 op(_CHECK_FUNCTION, (func_version/2 -- )) {
assert(PyFunction_Check(frame->f_funcobj));
DEOPT_IF(((PyFunctionObject *)frame->f_funcobj)->func_version != func_version);
Expand Down
52 changes: 52 additions & 0 deletions Python/executor_cases.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading