]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-126835: Move constant subscript folding to CFG (#129568)
authorYan Yanchii <yyanchiy@gmail.com>
Tue, 4 Feb 2025 08:10:55 +0000 (09:10 +0100)
committerGitHub <noreply@github.com>
Tue, 4 Feb 2025 08:10:55 +0000 (10:10 +0200)
Move folding of constant subscription from AST optimizer to CFG.

Co-authored-by: Irit Katriel <1055913+iritkatriel@users.noreply.github.com>
Include/internal/pycore_long.h
Lib/test/test_ast/test_ast.py
Lib/test/test_peepholer.py
Python/ast_opt.c
Python/codegen.c
Python/flowgraph.c

index c52eb77692dd6a2a4a06f4e36ee6bc63d78b8115..df0656a7cb8f0c7c376be2e7051dc07b2bbc9ae4 100644 (file)
@@ -65,6 +65,8 @@ PyAPI_FUNC(void) _PyLong_ExactDealloc(PyObject *self);
 #  error "_PY_NSMALLPOSINTS must be greater than or equal to 257"
 #endif
 
+#define _PY_IS_SMALL_INT(val) ((val) >= 0 && (val) < 256 && (val) < _PY_NSMALLPOSINTS)
+
 // Return a reference to the immortal zero singleton.
 // The function cannot return NULL.
 static inline PyObject* _PyLong_GetZero(void)
index c268a1f00f938e11fa02f856d48b5771e1206252..a438c8e81e4fd12ed1bed32ae3a9077809b74c7f 100644 (file)
@@ -3279,16 +3279,6 @@ class ASTOptimiziationTests(unittest.TestCase):
 
             self.assert_ast(code % (left, right), non_optimized_target, optimized_target)
 
-    def test_folding_subscript(self):
-        code = "(1,)[0]"
-
-        non_optimized_target = self.wrap_expr(
-            ast.Subscript(value=ast.Tuple(elts=[ast.Constant(value=1)]), slice=ast.Constant(value=0))
-        )
-        optimized_target = self.wrap_expr(ast.Constant(value=1))
-
-        self.assert_ast(code, non_optimized_target, optimized_target)
-
     def test_folding_type_param_in_function_def(self):
         code = "def foo[%s = 1 + 1](): pass"
 
index b5b2b350e77a3b3ca548e26fb18beb69b778aab1..9f2f9350d746610794d3b17b423ac90ec03e76e6 100644 (file)
@@ -473,6 +473,59 @@ class TestTranforms(BytecodeTestCase):
                     self.assertFalse(instr.opname.startswith('BUILD_'))
                 self.check_lnotab(code)
 
+    def test_constant_folding_small_int(self):
+        tests = [
+            # subscript
+            ('(0, )[0]', 0),
+            ('(1 + 2, )[0]', 3),
+            ('(2 + 2 * 2, )[0]', 6),
+            ('(1, (1 + 2 + 3, ))[1][0]', 6),
+            ('(255, )[0]', 255),
+            ('(256, )[0]', None),
+            ('(1000, )[0]', None),
+            ('(1 - 2, )[0]', None),
+        ]
+        for expr, oparg in tests:
+            with self.subTest(expr=expr, oparg=oparg):
+                code = compile(expr, '', 'single')
+                if oparg is not None:
+                    self.assertInBytecode(code, 'LOAD_SMALL_INT', oparg)
+                else:
+                    self.assertNotInBytecode(code, 'LOAD_SMALL_INT')
+                self.check_lnotab(code)
+
+    def test_folding_subscript(self):
+        tests = [
+            ('(1, )[0]', False),
+            ('(1, )[-1]', False),
+            ('(1 + 2, )[0]', False),
+            ('(1, (1, 2))[1][1]', False),
+            ('(1, 2)[2-1]', False),
+            ('(1, (1, 2))[1][2-1]', False),
+            ('(1, (1, 2))[1:6][0][2-1]', False),
+            ('"a"[0]', False),
+            ('("a" + "b")[1]', False),
+            ('("a" + "b", )[0][1]', False),
+            ('("a" * 10)[9]', False),
+            ('(1, )[1]', True),
+            ('(1, )[-2]', True),
+            ('"a"[1]', True),
+            ('"a"[-2]', True),
+            ('("a" + "b")[2]', True),
+            ('("a" + "b", )[0][2]', True),
+            ('("a" + "b", )[1][0]', True),
+            ('("a" * 10)[10]', True),
+            ('(1, (1, 2))[2:6][0][2-1]', True),
+        ]
+        for expr, has_error in tests:
+            with self.subTest(expr=expr, has_error=has_error):
+                code = compile(expr, '', 'single')
+                if not has_error:
+                    self.assertNotInBytecode(code, 'BINARY_SUBSCR')
+                else:
+                    self.assertInBytecode(code, 'BINARY_SUBSCR')
+                self.check_lnotab(code)
+
     def test_in_literal_list(self):
         def containtest():
             return x in [a, b]
index 01e208b88eca8be32b9c28496b6108379d55ca67..78d84002d593fb198f4aad011ff7e7899e759232 100644 (file)
@@ -567,25 +567,6 @@ fold_tuple(expr_ty node, PyArena *arena, _PyASTOptimizeState *state)
     return make_const(node, newval, arena);
 }
 
-static int
-fold_subscr(expr_ty node, PyArena *arena, _PyASTOptimizeState *state)
-{
-    PyObject *newval;
-    expr_ty arg, idx;
-
-    arg = node->v.Subscript.value;
-    idx = node->v.Subscript.slice;
-    if (node->v.Subscript.ctx != Load ||
-            arg->kind != Constant_kind ||
-            idx->kind != Constant_kind)
-    {
-        return 1;
-    }
-
-    newval = PyObject_GetItem(arg->v.Constant.value, idx->v.Constant.value);
-    return make_const(node, newval, arena);
-}
-
 /* Change literal list or set of constants into constant
    tuple or frozenset respectively.  Change literal list of
    non-constants into tuple.
@@ -822,7 +803,6 @@ astfold_expr(expr_ty node_, PyArena *ctx_, _PyASTOptimizeState *state)
     case Subscript_kind:
         CALL(astfold_expr, expr_ty, node_->v.Subscript.value);
         CALL(astfold_expr, expr_ty, node_->v.Subscript.slice);
-        CALL(fold_subscr, expr_ty, node_);
         break;
     case Starred_kind:
         CALL(astfold_expr, expr_ty, node_->v.Starred.value);
index 0bf9526cdc84352ac632914cfa0c8a82d2c530f8..e9853d7302f67fbd89ecbc3cd005488e2c593cfd 100644 (file)
@@ -284,7 +284,7 @@ codegen_addop_load_const(compiler *c, location loc, PyObject *o)
     if (PyLong_CheckExact(o)) {
         int overflow;
         long val = PyLong_AsLongAndOverflow(o, &overflow);
-        if (!overflow && val >= 0 && val < 256 && val < _PY_NSMALLPOSINTS) {
+        if (!overflow && _PY_IS_SMALL_INT(val)) {
             ADDOP_I(c, loc, LOAD_SMALL_INT, val);
             return SUCCESS;
         }
index a0b76050fd4af6d299861904eaaadab4dd5faa01..9ca7fadb8d7665e003f952e32f948c1c204d4d23 100644 (file)
@@ -6,6 +6,7 @@
 #include "pycore_compile.h"
 #include "pycore_intrinsics.h"
 #include "pycore_pymem.h"         // _PyMem_IsPtrFreed()
+#include "pycore_long.h"          // _PY_IS_SMALL_INT()
 
 #include "pycore_opcode_utils.h"
 #include "pycore_opcode_metadata.h" // OPCODE_HAS_ARG, etc
@@ -1443,6 +1444,84 @@ optimize_if_const_list_or_set(PyObject *const_cache, cfg_instr* inst, int n, PyO
     return SUCCESS;
 }
 
+/*
+  Walk basic block upwards starting from "start" to collect instruction pair
+  that loads consts skipping NOP's in between.
+*/
+static bool
+find_load_const_pair(basicblock *bb, int start, cfg_instr **first, cfg_instr **second)
+{
+    cfg_instr *second_load_const = NULL;
+    while (start >= 0) {
+        cfg_instr *inst = &bb->b_instr[start--];
+        if (inst->i_opcode == NOP) {
+            continue;
+        }
+        if (!loads_const(inst->i_opcode)) {
+            return false;
+        }
+        if (second_load_const == NULL) {
+            second_load_const = inst;
+            continue;
+        }
+        *first = inst;
+        *second = second_load_const;
+        return true;
+    }
+    return false;
+}
+
+/* Determine opcode & oparg for freshly folded constant. */
+static int
+newop_from_folded(PyObject *newconst, PyObject *consts,
+                  PyObject *const_cache, int *newopcode, int *newoparg)
+{
+    if (PyLong_CheckExact(newconst)) {
+        int overflow;
+        long val = PyLong_AsLongAndOverflow(newconst, &overflow);
+        if (!overflow && _PY_IS_SMALL_INT(val)) {
+            *newopcode = LOAD_SMALL_INT;
+            *newoparg = val;
+            return SUCCESS;
+        }
+    }
+    *newopcode = LOAD_CONST;
+    *newoparg = add_const(newconst, consts, const_cache);
+    RETURN_IF_ERROR(*newoparg);
+    return SUCCESS;
+}
+
+static int
+optimize_if_const_subscr(basicblock *bb, int n, PyObject *consts, PyObject *const_cache)
+{
+    cfg_instr *subscr = &bb->b_instr[n];
+    assert(subscr->i_opcode == BINARY_SUBSCR);
+    cfg_instr *arg, *idx;
+    if (!find_load_const_pair(bb, n-1, &arg, &idx)) {
+        return SUCCESS;
+    }
+    PyObject *o, *key;
+    if ((o = get_const_value(arg->i_opcode, arg->i_oparg, consts)) == NULL
+        || (key = get_const_value(idx->i_opcode, idx->i_oparg, consts)) == NULL)
+    {
+        return ERROR;
+    }
+    PyObject *newconst = PyObject_GetItem(o, key);
+    if (newconst == NULL) {
+        if (PyErr_ExceptionMatches(PyExc_KeyboardInterrupt)) {
+            return ERROR;
+        }
+        PyErr_Clear();
+        return SUCCESS;
+    }
+    int newopcode, newoparg;
+    RETURN_IF_ERROR(newop_from_folded(newconst, consts, const_cache, &newopcode, &newoparg));
+    INSTR_SET_OP1(subscr, newopcode, newoparg);
+    INSTR_SET_OP0(arg, NOP);
+    INSTR_SET_OP0(idx, NOP);
+    return SUCCESS;
+}
+
 #define VISITED (-1)
 
 // Replace an arbitrary run of SWAPs and NOPs with an optimal one that has the
@@ -1948,6 +2027,9 @@ optimize_basic_block(PyObject *const_cache, basicblock *bb, PyObject *consts)
                     INSTR_SET_OP0(inst, NOP);
                 }
                 break;
+            case BINARY_SUBSCR:
+                RETURN_IF_ERROR(optimize_if_const_subscr(bb, i, consts, const_cache));
+                break;
         }
     }