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-91053: Add an optional callback that is invoked whenever a function is modified #98175

Merged
merged 16 commits into from
Nov 22, 2022

Conversation

mpage
Copy link
Contributor

@mpage mpage commented Oct 11, 2022

JIT compilers may need to invalidate compiled code when a function is modified (e.g. if its code object is modified). This adds the ability to set a callback that, when set, is called each time a function is modified.

JIT compilers may need to invalidate compiled code when a function is
modified (e.g. if its code object is modified). This adds the ability
to set a callback that, when set, is called each time a function is
modified.
@mpage mpage changed the title Add an optional callback that is invoked whenever a function is modified gh-91053: Add an optional callback that is invoked whenever a function is modified Oct 11, 2022
@mpage
Copy link
Contributor Author

mpage commented Oct 12, 2022

pyperformance results are perf-neutral.

I also wrote a microbenchmark that attempts to measure the overhead on function creation when no callbacks are present. Happy to use a different benchmark and/or do this a bit more rigorously if there is concern. Results on the microbenchmark are perf-neutral as well:

This PR with no function watchers set:

mpage@ip-172-31-41-159:~$ ./cpython/python bench_comprehensions.py
0.08847772800072562
mpage@ip-172-31-41-159:~$ ./cpython/python bench_comprehensions.py
0.09016199100005906
mpage@ip-172-31-41-159:~$ ./cpython/python bench_comprehensions.py
0.08949500100061414
mpage@ip-172-31-41-159:~$ ./cpython/python bench_comprehensions.py
0.09101104000001214
mpage@ip-172-31-41-159:~$ ./cpython/python bench_comprehensions.py
0.08842268300213618
mpage@ip-172-31-41-159:~$

Upstream:

mpage@ip-172-31-41-159:~$ ./upstream/python bench_comprehensions.py
0.08706012900074711
mpage@ip-172-31-41-159:~$ ./upstream/python bench_comprehensions.py
0.09116156500022043
mpage@ip-172-31-41-159:~$ ./upstream/python bench_comprehensions.py
0.08924523199675605
mpage@ip-172-31-41-159:~$ ./upstream/python bench_comprehensions.py
0.0921605939984147
mpage@ip-172-31-41-159:~$ ./upstream/python bench_comprehensions.py
0.08725003300060052

All benchmarks were run on a bare metal machine in AWS that was configured using pyperf system tune.

@mpage mpage marked this pull request as ready for review October 12, 2022 15:49
@mpage mpage requested a review from a team as a code owner October 12, 2022 15:49
@mpage
Copy link
Contributor Author

mpage commented Oct 12, 2022

The failing test appears unrelated to this diff. Is there a way that I can re-run the test without adding additional commits?

@erlend-aasland
Copy link
Contributor

The failing test appears unrelated to this diff. Is there a way that I can re-run the test without adding additional commits?

You can merge in main

@zooba
Copy link
Member

zooba commented Oct 14, 2022

You can merge in main

Or any core dev (and probably triager) can trigger a rerun, which I did already and it's now passed.

Copy link
Contributor

@erlend-aasland erlend-aasland left a comment

Choose a reason for hiding this comment

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

Looks good. I added a bunch of PEP 7 comments because we should (almost) always strive to follow the style guide, both for new code and (in most cases) when modifying existing code. I also added some comments regarding docs, inconsistent naming of the constants, and some C API suggestions.

Questions:

  • PyFunction_SetDefaults should emit a PyFunction_EVENT_MODIFY_DEFAULTS event, no?
  • PyFunction_SetKwDefaults should emit a PyFunction_EVENT_MODIFY_KWDEFAULTS event, no?
  • Why no events for func_closure and func_annotations modifications?

Also, please add the Python test code to Lib/test/test_capi.py instead of adding a new Lib/test/test_func_events.py, like the dict watchers did.

Include/cpython/funcobject.h Outdated Show resolved Hide resolved
Include/cpython/funcobject.h Show resolved Hide resolved
Modules/_testcapi/func_events.c Outdated Show resolved Hide resolved
Modules/_testcapi/func_events.c Outdated Show resolved Hide resolved
Modules/_testcapi/func_events.c Outdated Show resolved Hide resolved
Doc/c-api/function.rst Outdated Show resolved Hide resolved
Doc/c-api/function.rst Outdated Show resolved Hide resolved
Doc/c-api/function.rst Outdated Show resolved Hide resolved
Doc/c-api/function.rst Outdated Show resolved Hide resolved
Objects/funcobject.c Show resolved Hide resolved
@mpage
Copy link
Contributor Author

mpage commented Nov 8, 2022

  • PyFunction_SetDefaults should emit a PyFunction_EVENT_MODIFY_DEFAULTS event, no?
  • PyFunction_SetKwDefaults should emit a PyFunction_EVENT_MODIFY_KWDEFAULTS event, no?

Yep, good catch!

  • Why no events for func_closure and func_annotations modifications?

func_closure is read-only and func_annotations isn't useful for our optimization purposes.

Also, please add the Python test code to Lib/test/test_capi.py instead of adding a new Lib/test/test_func_events.py, like the dict watchers did.

Sure thing, will do.

Copy link
Contributor

@erlend-aasland erlend-aasland left a comment

Choose a reason for hiding this comment

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

Thanks for making the changes! I've got one last minor style nit. Also please merge in main and resolve the conflicts.

Modules/_testcapi/func_events.c Outdated Show resolved Hide resolved
@markshannon
Copy link
Member

I don't like having watchers for function creation and destruction.

Having those watchers adds overhead for every creation of a comprehension or nested function, which could be an issue for performance for two reasons:

  1. The overhead. While the overhead is hard to measure now, that is more a comment on how inefficient function object creation currently is, rather than the overhead being negligible.
  2. It prevents the removal of functions by the optimizer. For example, there is no reason for a function to be created for a list comprehension. An optimizer could avoid creating the function and preserve semantics. If creation of a function is always observable, this optimization becomes impossible.

@carljm
Copy link
Member

carljm commented Nov 16, 2022

Discussed offline with @markshannon and reached these conclusions (Mark please let me know if any of this doesn't match your understanding):

  1. In the long term it might be better if instead of setting function vectorcall (and thus needing func-created hook so we can set it), we could rely on the core runtime to tell us when a function is hot and defer to our execution in that case. (This can be made compatible with pre-fork compilation if we eagerly compile code objects when they are created, and then just pull the pointer to the compiled code out of our cache when the runtime says "this function is hot, please execute it for me.") But this is all hypothetical at the moment and still requires a lot of design, implementation, and verification that it actually works, so it's a bit premature to rely on this solution.
  2. The "observability of function creation" issue doesn't need to be a blocker here. The documentation for the hook can be clear that optimized execution of Python code is free to elide function-object creation where possible, and if function object creation is optimized away, no function-created hook will be fired, and this is not considered a semantic change.
  3. The overhead here really isn't much at all compared to the existing work of creating a function object. Let's try to optimize it a bit more so that in the no-watchers case it's just a single read/test (per Mark's inline comment), and that'll be good enough.

We also discussed a possible alternative design where code objects have a vectorcall pointer and function objects just copy it from the code object; then we wouldn't need func-created hook. But this also adds an extra read to function creation, as well as making code objects bigger, so it's not clear this is really a better approach.

mpage and others added 4 commits November 21, 2022 20:47
A bit is set in the bit vector iff there is a watcher set at the
corresponding offset in the watcher array. Only notify watchers
if at least one bit is set.
@erlend-aasland
Copy link
Contributor

LGTM, thanks! I'll wait for @markshannon's thumbs up before merging.

@markshannon markshannon self-requested a review November 22, 2022 11:33
@erlend-aasland erlend-aasland merged commit 3db0a21 into python:main Nov 22, 2022
facebook-github-bot pushed a commit to facebookincubator/cinder that referenced this pull request Jun 27, 2023
Summary:
Backport function watchers from upstream 3.12 (cf python/cpython#98175, although I backported from latest 3.12 branch, which has had a few modifications.)

This diff doesn't use the new API yet; I'll do that in a separate stacked diff.

Reviewed By: DinoV

Differential Revision: D46993187

fbshipit-source-id: 35f3a0f86f6f0e2764205d4c865e1fb5efb69314
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants