summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Lib/test/test_peepholer.py67
-rw-r--r--Misc/NEWS.d/next/Core and Builtins/2022-01-27-14-20-18.bpo-45828.kzk4fl.rst2
-rw-r--r--Python/compile.c67
3 files changed, 136 insertions, 0 deletions
diff --git a/Lib/test/test_peepholer.py b/Lib/test/test_peepholer.py
index 2df5883..6f24b29 100644
--- a/Lib/test/test_peepholer.py
+++ b/Lib/test/test_peepholer.py
@@ -1,10 +1,25 @@
import dis
from itertools import combinations, product
+import textwrap
import unittest
from test.support.bytecode_helper import BytecodeTestCase
+def compile_pattern_with_fast_locals(pattern):
+ source = textwrap.dedent(
+ f"""
+ def f(x):
+ match x:
+ case {pattern}:
+ pass
+ """
+ )
+ namespace = {}
+ exec(source, namespace)
+ return namespace["f"].__code__
+
+
def count_instr_recursively(f, opname):
count = 0
for instr in dis.get_instructions(f):
@@ -580,6 +595,58 @@ class TestTranforms(BytecodeTestCase):
'not all arguments converted during string formatting'):
eval("'%s, %s' % (x, *y)", {'x': 1, 'y': [2, 3]})
+ def test_static_swaps_unpack_two(self):
+ def f(a, b):
+ a, b = a, b
+ b, a = a, b
+ self.assertNotInBytecode(f, "SWAP")
+
+ def test_static_swaps_unpack_three(self):
+ def f(a, b, c):
+ a, b, c = a, b, c
+ a, c, b = a, b, c
+ b, a, c = a, b, c
+ b, c, a = a, b, c
+ c, a, b = a, b, c
+ c, b, a = a, b, c
+ self.assertNotInBytecode(f, "SWAP")
+
+ def test_static_swaps_match_mapping(self):
+ for a, b, c in product("_a", "_b", "_c"):
+ pattern = f"{{'a': {a}, 'b': {b}, 'c': {c}}}"
+ with self.subTest(pattern):
+ code = compile_pattern_with_fast_locals(pattern)
+ self.assertNotInBytecode(code, "SWAP")
+
+ def test_static_swaps_match_class(self):
+ forms = [
+ "C({}, {}, {})",
+ "C({}, {}, c={})",
+ "C({}, b={}, c={})",
+ "C(a={}, b={}, c={})"
+ ]
+ for a, b, c in product("_a", "_b", "_c"):
+ for form in forms:
+ pattern = form.format(a, b, c)
+ with self.subTest(pattern):
+ code = compile_pattern_with_fast_locals(pattern)
+ self.assertNotInBytecode(code, "SWAP")
+
+ def test_static_swaps_match_sequence(self):
+ swaps = {"*_, b, c", "a, *_, c", "a, b, *_"}
+ forms = ["{}, {}, {}", "{}, {}, *{}", "{}, *{}, {}", "*{}, {}, {}"]
+ for a, b, c in product("_a", "_b", "_c"):
+ for form in forms:
+ pattern = form.format(a, b, c)
+ with self.subTest(pattern):
+ code = compile_pattern_with_fast_locals(pattern)
+ if pattern in swaps:
+ # If this fails... great! Remove this pattern from swaps
+ # to prevent regressing on any improvement:
+ self.assertInBytecode(code, "SWAP")
+ else:
+ self.assertNotInBytecode(code, "SWAP")
+
class TestBuglets(unittest.TestCase):
diff --git a/Misc/NEWS.d/next/Core and Builtins/2022-01-27-14-20-18.bpo-45828.kzk4fl.rst b/Misc/NEWS.d/next/Core and Builtins/2022-01-27-14-20-18.bpo-45828.kzk4fl.rst
new file mode 100644
index 0000000..687fef0
--- /dev/null
+++ b/Misc/NEWS.d/next/Core and Builtins/2022-01-27-14-20-18.bpo-45828.kzk4fl.rst
@@ -0,0 +1,2 @@
+The bytecode compiler now attempts to apply runtime stack manipulations at
+compile-time (whenever it is feasible to do so).
diff --git a/Python/compile.c b/Python/compile.c
index 5fcaa0a..bfe451b 100644
--- a/Python/compile.c
+++ b/Python/compile.c
@@ -8472,6 +8472,72 @@ swaptimize(basicblock *block, int *ix)
return 0;
}
+// This list is pretty small, since it's only okay to reorder opcodes that:
+// - can't affect control flow (like jumping or raising exceptions)
+// - can't invoke arbitrary code (besides finalizers)
+// - only touch the TOS (and pop it when finished)
+#define SWAPPABLE(opcode) \
+ ((opcode) == STORE_FAST || (opcode) == POP_TOP)
+
+static int
+next_swappable_instruction(basicblock *block, int i, int lineno)
+{
+ while (++i < block->b_iused) {
+ struct instr *instruction = &block->b_instr[i];
+ if (0 <= lineno && instruction->i_lineno != lineno) {
+ // Optimizing across this instruction could cause user-visible
+ // changes in the names bound between line tracing events!
+ return -1;
+ }
+ if (instruction->i_opcode == NOP) {
+ continue;
+ }
+ if (SWAPPABLE(instruction->i_opcode)) {
+ return i;
+ }
+ return -1;
+ }
+ return -1;
+}
+
+// Attempt to apply SWAPs statically by swapping *instructions* rather than
+// stack items. For example, we can replace SWAP(2), POP_TOP, STORE_FAST(42)
+// with the more efficient NOP, STORE_FAST(42), POP_TOP.
+static void
+apply_static_swaps(basicblock *block, int i)
+{
+ // SWAPs are to our left, and potential swaperands are to our right:
+ for (; 0 <= i; i--) {
+ assert(i < block->b_iused);
+ struct instr *swap = &block->b_instr[i];
+ if (swap->i_opcode != SWAP) {
+ if (swap->i_opcode == NOP || SWAPPABLE(swap->i_opcode)) {
+ // Nope, but we know how to handle these. Keep looking:
+ continue;
+ }
+ // We can't reason about what this instruction does. Bail:
+ return;
+ }
+ int j = next_swappable_instruction(block, i, -1);
+ if (j < 0) {
+ return;
+ }
+ int k = j;
+ int lineno = block->b_instr[j].i_lineno;
+ for (int count = swap->i_oparg - 1; 0 < count; count--) {
+ k = next_swappable_instruction(block, k, lineno);
+ if (k < 0) {
+ return;
+ }
+ }
+ // Success!
+ swap->i_opcode = NOP;
+ struct instr temp = block->b_instr[j];
+ block->b_instr[j] = block->b_instr[k];
+ block->b_instr[k] = temp;
+ }
+}
+
// Attempt to eliminate jumps to jumps by updating inst to jump to
// target->i_target using the provided opcode. Return whether or not the
// optimization was successful.
@@ -8714,6 +8780,7 @@ optimize_basic_block(struct compiler *c, basicblock *bb, PyObject *consts)
if (swaptimize(bb, &i)) {
goto error;
}
+ apply_static_swaps(bb, i);
break;
case KW_NAMES:
break;