mirror of https://github.com/python/cpython
bpo-46528: Attempt SWAPs at compile-time (GH-30970)
This commit is contained in:
parent
5a3f97291e
commit
78ae4cc6dc
|
@ -1,10 +1,25 @@
|
||||||
import dis
|
import dis
|
||||||
from itertools import combinations, product
|
from itertools import combinations, product
|
||||||
|
import textwrap
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from test.support.bytecode_helper import BytecodeTestCase
|
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):
|
def count_instr_recursively(f, opname):
|
||||||
count = 0
|
count = 0
|
||||||
for instr in dis.get_instructions(f):
|
for instr in dis.get_instructions(f):
|
||||||
|
@ -580,6 +595,58 @@ class TestTranforms(BytecodeTestCase):
|
||||||
'not all arguments converted during string formatting'):
|
'not all arguments converted during string formatting'):
|
||||||
eval("'%s, %s' % (x, *y)", {'x': 1, 'y': [2, 3]})
|
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):
|
class TestBuglets(unittest.TestCase):
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
The bytecode compiler now attempts to apply runtime stack manipulations at
|
||||||
|
compile-time (whenever it is feasible to do so).
|
|
@ -8472,6 +8472,72 @@ swaptimize(basicblock *block, int *ix)
|
||||||
return 0;
|
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
|
// Attempt to eliminate jumps to jumps by updating inst to jump to
|
||||||
// target->i_target using the provided opcode. Return whether or not the
|
// target->i_target using the provided opcode. Return whether or not the
|
||||||
// optimization was successful.
|
// optimization was successful.
|
||||||
|
@ -8714,6 +8780,7 @@ optimize_basic_block(struct compiler *c, basicblock *bb, PyObject *consts)
|
||||||
if (swaptimize(bb, &i)) {
|
if (swaptimize(bb, &i)) {
|
||||||
goto error;
|
goto error;
|
||||||
}
|
}
|
||||||
|
apply_static_swaps(bb, i);
|
||||||
break;
|
break;
|
||||||
case KW_NAMES:
|
case KW_NAMES:
|
||||||
break;
|
break;
|
||||||
|
|
Loading…
Reference in New Issue