Skip to content

gh-109039: Branch prediction for Tier 2 interpreter #109038

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Sep 11, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 12 additions & 8 deletions Include/internal/pycore_opcode_metadata.h

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

12 changes: 12 additions & 0 deletions Lib/opcode.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,18 @@
"counter": 1,
"version": 2,
},
"POP_JUMP_IF_TRUE": {
"counter": 1,
},
"POP_JUMP_IF_FALSE": {
"counter": 1,
},
"POP_JUMP_IF_NONE": {
"counter": 1,
},
"POP_JUMP_IF_NOT_NONE": {
"counter": 1,
},
}

_inline_cache_entries = {
Expand Down
219 changes: 111 additions & 108 deletions Lib/test/test_dis.py

Large diffs are not rendered by default.

16 changes: 8 additions & 8 deletions Lib/test/test_monitoring.py
Original file line number Diff line number Diff line change
Expand Up @@ -1348,10 +1348,10 @@ def func():

self.check_events(func, recorders = JUMP_AND_BRANCH_RECORDERS, expected = [
('branch', 'func', 2, 2),
('branch', 'func', 3, 6),
('branch', 'func', 3, 4),
('jump', 'func', 6, 2),
('branch', 'func', 2, 2),
('branch', 'func', 3, 4),
('branch', 'func', 3, 3),
('jump', 'func', 4, 2),
('branch', 'func', 2, 2)])

Expand All @@ -1361,13 +1361,13 @@ def func():
('line', 'func', 2),
('branch', 'func', 2, 2),
('line', 'func', 3),
('branch', 'func', 3, 6),
('branch', 'func', 3, 4),
('line', 'func', 6),
('jump', 'func', 6, 2),
('line', 'func', 2),
('branch', 'func', 2, 2),
('line', 'func', 3),
('branch', 'func', 3, 4),
('branch', 'func', 3, 3),
('line', 'func', 4),
('jump', 'func', 4, 2),
('line', 'func', 2),
Expand Down Expand Up @@ -1400,8 +1400,8 @@ def func():
('line', 'func', 5),
('line', 'meth', 1),
('jump', 'func', 5, 5),
('jump', 'func', 5, '[offset=112]'),
('branch', 'func', '[offset=118]', '[offset=120]'),
('jump', 'func', 5, '[offset=114]'),
('branch', 'func', '[offset=120]', '[offset=122]'),
('line', 'get_events', 11)])

self.check_events(func, recorders = FLOW_AND_LINE_RECORDERS, expected = [
Expand All @@ -1416,8 +1416,8 @@ def func():
('line', 'meth', 1),
('return', None),
('jump', 'func', 5, 5),
('jump', 'func', 5, '[offset=112]'),
('branch', 'func', '[offset=118]', '[offset=120]'),
('jump', 'func', 5, '[offset=114]'),
('branch', 'func', '[offset=120]', '[offset=122]'),
('return', None),
('line', 'get_events', 11)])

Expand Down
47 changes: 34 additions & 13 deletions Python/bytecodes.c
Original file line number Diff line number Diff line change
Expand Up @@ -2253,14 +2253,22 @@ dummy_func(
goto resume_frame;
}

inst(POP_JUMP_IF_FALSE, (cond -- )) {
inst(POP_JUMP_IF_FALSE, (unused/1, cond -- )) {
assert(PyBool_Check(cond));
JUMPBY(oparg * Py_IsFalse(cond));
int flag = Py_IsFalse(cond);
#if ENABLE_SPECIALIZATION
next_instr->cache = (next_instr->cache << 1) | flag;
#endif
JUMPBY(oparg * flag);
}

inst(POP_JUMP_IF_TRUE, (cond -- )) {
inst(POP_JUMP_IF_TRUE, (unused/1, cond -- )) {
assert(PyBool_Check(cond));
JUMPBY(oparg * Py_IsTrue(cond));
int flag = Py_IsTrue(cond);
#if ENABLE_SPECIALIZATION
next_instr->cache = (next_instr->cache << 1) | flag;
#endif
JUMPBY(oparg * flag);
Copy link
Member

Choose a reason for hiding this comment

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

Don't you need also a SKIP_OVER the cache?

I'm guessing that could cause the assert(frame->prev_instr == instr); in _Py_call_instrumentation_jump to fail for the instrumented jumps.

Copy link
Member Author

Choose a reason for hiding this comment

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

That skip over the cache is already generated -- see the corresponding code in generated_cases.c.h.

Copy link
Member

Choose a reason for hiding this comment

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

You mean the "next_instr += 1;"? In all other cases there is an explicit SKIP_OVER(INLINE_CACHE_ENTRIES_...) in bytecodes.c.

Copy link
Member Author

Choose a reason for hiding this comment

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

Those SKIP_OVER() calls are always followed by a DISPATCH() call (or maybe a goto).

Copy link
Member

Choose a reason for hiding this comment

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

I see. Can we make the code generator emit SKIP_OVER(X) instead of next_instr += x;?

Copy link
Member Author

Choose a reason for hiding this comment

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

We can, though IIRC Mark at some point objected to emitting macros. So I'd rather keep the status quo.

Copy link
Member

Choose a reason for hiding this comment

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

@markshannon What is the reason not to emit macros?

A reason to emit them is so that they are implemented in one place, so if their implementation changes you only change there. Do we want to change the code generator (and to remember that we need to) every time the implementation of a macro like SKIP_OVER changes?

Copy link
Member Author

Choose a reason for hiding this comment

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

Honestly I don't expect SKIP_OVER() to ever change. In hand-written code the macro expresses the intent better. But in generated code it just obscures what happens. I had to go to some lengths to change PEEK() and POKE() calls in the generated code to using stack_pointer[x] instead; I don't want to go back. If you still disagree, try engaging @markshannon.

Copy link
Member

Choose a reason for hiding this comment

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

If you still disagree,

More like trying to understand than disagreeing.

try engaging @markshannon.

Yes I directed my previous comment to him.

}

op(IS_NONE, (value -- b)) {
Expand Down Expand Up @@ -3712,47 +3720,60 @@ dummy_func(
INSTRUMENTED_JUMP(next_instr-1, next_instr+1-oparg, PY_MONITORING_EVENT_JUMP);
}

inst(INSTRUMENTED_POP_JUMP_IF_TRUE, ( -- )) {
inst(INSTRUMENTED_POP_JUMP_IF_TRUE, (unused/1 -- )) {
PyObject *cond = POP();
assert(PyBool_Check(cond));
_Py_CODEUNIT *here = next_instr - 1;
int offset = Py_IsTrue(cond) * oparg;
int flag = Py_IsTrue(cond);
int offset = flag * oparg;
#if ENABLE_SPECIALIZATION
next_instr->cache = (next_instr->cache << 1) | flag;
#endif
INSTRUMENTED_JUMP(here, next_instr + offset, PY_MONITORING_EVENT_BRANCH);
}

inst(INSTRUMENTED_POP_JUMP_IF_FALSE, ( -- )) {
inst(INSTRUMENTED_POP_JUMP_IF_FALSE, (unused/1 -- )) {
PyObject *cond = POP();
assert(PyBool_Check(cond));
_Py_CODEUNIT *here = next_instr - 1;
int offset = Py_IsFalse(cond) * oparg;
int flag = Py_IsFalse(cond);
int offset = flag * oparg;
INSTRUMENTED_JUMP(here, next_instr + offset, PY_MONITORING_EVENT_BRANCH);
}

inst(INSTRUMENTED_POP_JUMP_IF_NONE, ( -- )) {
inst(INSTRUMENTED_POP_JUMP_IF_NONE, (unused/1 -- )) {
PyObject *value = POP();
_Py_CODEUNIT *here = next_instr-1;
_Py_CODEUNIT *here = next_instr - 1;
int flag = Py_IsNone(value);
int offset;
if (Py_IsNone(value)) {
if (flag) {
offset = oparg;
}
else {
Py_DECREF(value);
offset = 0;
}
#if ENABLE_SPECIALIZATION
next_instr->cache = (next_instr->cache << 1) | flag;
#endif
INSTRUMENTED_JUMP(here, next_instr + offset, PY_MONITORING_EVENT_BRANCH);
}

inst(INSTRUMENTED_POP_JUMP_IF_NOT_NONE, ( -- )) {
inst(INSTRUMENTED_POP_JUMP_IF_NOT_NONE, (unused/1 -- )) {
PyObject *value = POP();
_Py_CODEUNIT *here = next_instr-1;
int offset;
if (Py_IsNone(value)) {
int flag = Py_IsNone(value);
if (flag) {
offset = 0;
}
else {
Py_DECREF(value);
offset = oparg;
}
#if ENABLE_SPECIALIZATION
next_instr->cache = (next_instr->cache << 1) | !flag;
#endif
INSTRUMENTED_JUMP(here, next_instr + offset, PY_MONITORING_EVENT_BRANCH);
}

Expand Down
55 changes: 46 additions & 9 deletions Python/generated_cases.c.h

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

17 changes: 15 additions & 2 deletions Tools/cases_generator/generate_cases.py
Original file line number Diff line number Diff line change
Expand Up @@ -549,10 +549,23 @@ def write_metadata(self, metadata_filename: str, pymetadata_filename: str) -> No
"=",
";",
):
for name, _ in self.families.items():
family_member_names: set[str] = set()
for name, family in self.families.items():
family_member_names.update(family.members)
instr = self.instrs[name]
if instr.cache_offset > 0:
self.out.emit(f'[{name}] = {instr.cache_offset},')
self.out.emit(f"[{name}] = {instr.cache_offset},")
for instr in self.instrs.values():
if (
not instr.family
and instr.cache_offset
and instr.kind == "inst"
and not instr.name.startswith("INSTRUMENTED_")
):
self.out.emit(f"[{instr.name}] = {instr.cache_offset},")
for mac in self.macro_instrs.values():
if mac.name not in family_member_names and mac.cache_offset > 0:
self.out.emit(f"[{mac.name}] = {mac.cache_offset},")
# Irregular case:
self.out.emit('[JUMP_BACKWARD] = 1,')

Expand Down