Skip to content
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

GH-117086: Fix function version numbering #117093

Closed
wants to merge 2 commits into from

Conversation

markshannon
Copy link
Member

@markshannon markshannon commented Mar 20, 2024

Tracks not only the code object, but the globals and number of defaults when issuing function version numbers.

Fixes #117051

@markshannon
Copy link
Member Author

I need to add checks for builtins.

@markshannon markshannon reopened this Mar 20, 2024
@markshannon markshannon marked this pull request as ready for review March 20, 2024 19:35
Copy link
Member

@gvanrossum gvanrossum left a comment

Choose a reason for hiding this comment

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

I'm a little skeptical about some edge cases.

Comment on lines +3835 to +3857
PyCodeObject *code = (PyCodeObject *)codeobj;
if (func_obj->func_builtins != tstate->interp->builtins) {
_PyFunction_SetVersion(func_obj, 0);
}
else if (code->co_version == 0) {
code->co_version = tstate->interp->func_state.next_version;
if (tstate->interp->func_state.next_version != 0) {
tstate->interp->func_state.next_version++;
}
assert(code->co_expected_globals_version == 0);
if (PyDict_CheckExact(GLOBALS())) {
code->co_expected_globals_version = ((PyDictObject *)GLOBALS())->ma_version_tag;
}
_PyFunction_SetVersion(func_obj, code->co_version);
}
else {
if (PyDict_CheckExact(GLOBALS()) &&
code->co_expected_globals_version == ((PyDictObject *)GLOBALS())->ma_version_tag) {
_PyFunction_SetVersion(func_obj, code->co_version);
}
}
func = (PyObject *)func_obj;
Py_DECREF(codeobj);
Copy link
Member

Choose a reason for hiding this comment

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

This feels like an awfully large block of code for the JIT. Maybe it should be moved to a helper function?

Also, my brain is too small to fully grasp what will happen if we have a local function that references a global and is repeatedly created (maybe a generator expression inside a hot loop), when the referenced global changes (or some other global, given how dict versions work IIRC). Is that going to completely disable tier 2 for that hot loop, or is it just going to use _LOAD_GLOBAL_MODULE instead of _LOAD_CONST_INLINE?

A variant of the new test may reveal what happens more easily than trying to reason it through...

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 feels like an awfully large block of code for the JIT. Maybe it should be moved to a helper function?

Maybe, but that is the responsibility of the JIT builder. Let's not complicate the interpreter specification based on the current behavior of the JIT.

Is that going to completely disable tier 2 for that hot loop?

If the globals are changed, then we will get a different function version number.
That will disable some tier 2 optimizations. There is no reason why it should completely disable tier 2 optimization.

Copy link
Member

Choose a reason for hiding this comment

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

I would like to play with this to see what actually happens. As I said, my brain is too small to reason it through, and your response ("should") does not allay my worries.

Comment on lines +3881 to +3887
PyCodeObject *code = (PyCodeObject *)func_obj->func_code;
if (code->co_expected_number_of_defaults < 0) {
code->co_expected_number_of_defaults = (int32_t)PyTuple_GET_SIZE(attr);
}
else if (code->co_expected_number_of_defaults != PyTuple_GET_SIZE(attr)) {
func_obj->func_version = 0;
}
Copy link
Member

Choose a reason for hiding this comment

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

This works, but couldn't we let the bytecode compiler give us the number? Again, I worry a bit about the size of the JIT code.

@@ -78,6 +79,40 @@ def func(x=0):
)


class TestOptimizations(unittest.TestCase):

def test_globals_to_consts(self):
Copy link
Member

Choose a reason for hiding this comment

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

Note that this test only fails when tier 2 is selected (e.g. -X uops, or when the JIT is enabled).

We have machinery in test_capi/test_opt.py to force the optimizer on. It feels pretty arbitrary whether this test belongs here or there. Can you explain the criteria for where a tier-2-related test should go?

Copy link
Member Author

Choose a reason for hiding this comment

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

Can you explain the criteria for where a tier-2-related test should go?

I don't think there are any. This test should pass for tier-1 and tier-2, as should all tests.

Copy link
Member

Choose a reason for hiding this comment

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

Okay. In my mind this test should be moved to test_capi/test_opt.py and should be wrapped in with temporary_optimizer(_testinternalcapi.new_uop_optimizer()):. Otherwise it doesn't test anything unless build with the JIT or run with -Xuops.

@markshannon markshannon closed this Aug 6, 2024
@markshannon markshannon deleted the fix-function-versions branch August 6, 2024 10:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Incorrect output due to Tier 2 _LOAD_CONST_INLINE
2 participants