You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
add support for watching writes to selected dictionaries #91052
Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.
Show more details
GitHub fields:
assignee=Noneclosed_at=Nonecreated_at=<Date2022-03-01.22:19:05.128>labels= ['expert-C-API', '3.11']
title='add support for watching writes to selected dictionaries'updated_at=<Date2022-03-15.17:17:40.946>user='https://github.com/carljm'
CPython extensions providing optimized execution of Python bytecode (e.g. the Cinder JIT), or even CPython itself (e.g. the faster-cpython project) may wish to inline-cache access to frequently-read and rarely-changed namespaces, e.g. module globals. Rather than requiring a dict version guard on every cached read, the best-performing way to do this is is to mark the dictionary as “watched” and set a callback on writes to watched dictionaries. This optimizes the cached-read fast-path at a small cost to the (relatively infrequent and usually less perf sensitive) write path.
The intention in this issue is not to add any specific optimization or cache (yet); just the ability to mark a dictionary as “watched” and set a write callback.
The callback will be global, not per-dictionary (no extra function pointer stored in every dict). CPython will track only one global callback; it is a well-behaved client’s responsibility to check if a callback is already set when setting a new one, and daisy-chain to the previous callback if so. Given that multiple clients may mark dictionaries as watched, a dict watcher callback may receive events for dictionaries that were marked as watched by other clients, and should handle this gracefully.
There is no provision in the API for “un-watching” a watched dictionary; such an API could not be used safely in the face of potentially multiple dict-watching clients.
The Cinder implementation marks dictionaries as watched using the least bit of the dictionary version (so version increments by 2); this also avoids any additional memory usage for marking a dict as watched.
Initial proposed API, comments welcome:
// Mark given dictionary as "watched" (global callback will be called if it is modified)voidPyDict_Watch(PyObject*dict);
// Check if given dictionary is already watchedintPyDict_IsWatched(PyObject*dict);
typedefenum {
PYDICT_EVENT_CLEARED,
PYDICT_EVENT_DEALLOCED,
PYDICT_EVENT_MODIFIED
} PyDict_WatchEvent;
// Callback to be invoked when a watched dict is cleared, dealloced, or modified.// In clear/dealloc case, key and new_value will be NULL. Otherwise, new_value will be the// new value for key, NULL if key is being deleted.typedefvoid(*PyDict_WatchCallback)(PyDict_WatchEventevent, PyObject*dict, PyObject*key, PyObject*new_value);
// Set new global watch callback; supply NULL to clear callbackvoidPyDict_SetWatchCallback(PyDict_WatchCallbackcallback);
// Get existing global watch callbackPyDict_WatchCallbackPyDict_GetWatchCallback();
The callback will be called immediately before the modification to the dict takes effect, thus the callback will also have access to the prior state of the dict.
changed the title [-]add support for watching writes to selecting dictionaries[/-][+]add support for watching writes to selected dictionaries[/+]on Mar 1, 2022
changed the title [-]add support for watching writes to selecting dictionaries[/-][+]add support for watching writes to selected dictionaries[/+]on Mar 1, 2022
At first quick glance, this makes sense and the API looks reasonable.
Question: what happens on interpreter shutdown?
Shutdown obviously finalized and clears out most all dicts. I guess the C callback simply gets called for each of these? That makes sense. Just wondering if there are any ramifications of that. The callback is in C so it shouldn't have issues with this.
A pyperformance suite run on an interpreter modified to support this but having no callbacks registered would be useful. (basically judging if there is measurable overhead added by the watched bit check - I doubt it'll be noticeable in most code)
Thanks gps! Working on a PR and will collect pyperformance data as well.
We haven't observed any issues in Cinder with the callback just being called at shutdown, too, but if there are problems with that it should be possible to just have CPython clear the callback at shutdown time.
CPython will track only one global callback; it is a well-behaved client’s responsibility to check if a callback is already set when setting a new one, and daisy-chain to the previous callback if so.
Hm, this is a bit scary. Could we (or others) end up with unguarded stale caches if some buggy extension forgets to chain the calls correctly?
Core CPython seems most at risk of this, since we would most likely be registered first.
Could we (or others) end up with unguarded stale caches if some buggy extension forgets to chain the calls correctly?
Yes. I can really go either way on this. I initially opted for simplicity in the core support at the cost of asking a bit more of clients, on the theory that a) there are lots of ways for a buggy C extension to cause crashes with bad use of the C API, and b) I don't expect there to be very many extensions using this API. But it's also true that the consequences of a mistake here could be hard to debug (and easily blamed to the wrong place), and there might turn out to be more clients for dict-watching than I expect! If the consensus is to prefer CPython tracking an array of callbacks instead, we can try that.
when you say "only one global callback": does that mean per-interpreter, or per-process?
Good question! The currently proposed API suggests per-process, but it's not a question I've given a lot of thought to yet; open to suggestions. It seems like in general the preference is to avoid global state and instead tie things to an interpreter instance? I'll need to do a bit of research to understand exactly how that would affect the implementation. Doesn't seem like it should be a problem, though it might make the lookup at write time to see if we have a callback a bit slower.
If someone using this feature writes a buggy implementation of a callback that doesn't chain reliably, that is a bug in their code and all of the fallout from that is "just" a bug to be fixed in said code.
Think of it like a C signal handler, the OS doesn't chain for you - it is the signal handler installer's responsibility to chain to the previous one. Not an unusual pattern for callback hooks in C.
Those don't even provide for chaining. We could do the same here and not let a hook be set more than once instead of providing a Get API to allow for manual chaining. That seems less useful.
Getting a notification for every change of a global in module, is likely to make use the use of global variables extremely expensive.
var = 0
CONST = 1
def foo(...):
...
I may well want to be notified if foo or CONST gets modified, but performance could suffer badly if we make a callback every time var is changed.
--------------
What happens if a watched dictionary is modified in a callback?
--------------
How do you plan to implement this? Steal a bit from ma_version_tag or replace ma_version_tag?
If you replace ma_version_tag this could actually speed things up a tad.
You'd probably need a PEP to replace PEP-509, but I think this may need a PEP anyway.
Getting a notification for every change of a global in module, is likely to make use the use of global variables extremely expensive.
Perhaps a compromise is possible here: one global group/chain of callbacks registered for all dictionaries in an interpreter seems reasonable (to keep overhead low), but we could reduce the number of times it’s actually called by, say, tagging specific values of specific dictionaries to be watched.
For example, we could just tag the low bit of any pointer in a dictionary’s values that we want to be notified of changes to. Something like that seems like it could really keep the cost of watching down without sacrificing power.
Activity
carljm commentedon Mar 1, 2022
CPython extensions providing optimized execution of Python bytecode (e.g. the Cinder JIT), or even CPython itself (e.g. the faster-cpython project) may wish to inline-cache access to frequently-read and rarely-changed namespaces, e.g. module globals. Rather than requiring a dict version guard on every cached read, the best-performing way to do this is is to mark the dictionary as “watched” and set a callback on writes to watched dictionaries. This optimizes the cached-read fast-path at a small cost to the (relatively infrequent and usually less perf sensitive) write path.
We have an implementation of this in Cinder ( https://docs.google.com/document/d/1l8I-FDE1xrIShm9eSNJqsGmY_VanMDX5-aK_gujhYBI/edit#heading=h.n2fcxgq6ypwl ), used already by the Cinder JIT and its specializing interpreter. We would like to make the Cinder JIT available as a third-party extension to CPython ( https://docs.google.com/document/d/1l8I-FDE1xrIShm9eSNJqsGmY_VanMDX5-aK_gujhYBI/ ), and so we are interested in adding dict watchers to core CPython.
The intention in this issue is not to add any specific optimization or cache (yet); just the ability to mark a dictionary as “watched” and set a write callback.
The callback will be global, not per-dictionary (no extra function pointer stored in every dict). CPython will track only one global callback; it is a well-behaved client’s responsibility to check if a callback is already set when setting a new one, and daisy-chain to the previous callback if so. Given that multiple clients may mark dictionaries as watched, a dict watcher callback may receive events for dictionaries that were marked as watched by other clients, and should handle this gracefully.
There is no provision in the API for “un-watching” a watched dictionary; such an API could not be used safely in the face of potentially multiple dict-watching clients.
The Cinder implementation marks dictionaries as watched using the least bit of the dictionary version (so version increments by 2); this also avoids any additional memory usage for marking a dict as watched.
Initial proposed API, comments welcome:
The callback will be called immediately before the modification to the dict takes effect, thus the callback will also have access to the prior state of the dict.
[-]add support for watching writes to selecting dictionaries[/-][+]add support for watching writes to selected dictionaries[/+][-]add support for watching writes to selecting dictionaries[/-][+]add support for watching writes to selected dictionaries[/+]gpshead commentedon Mar 2, 2022
At first quick glance, this makes sense and the API looks reasonable.
Question: what happens on interpreter shutdown?
Shutdown obviously finalized and clears out most all dicts. I guess the C callback simply gets called for each of these? That makes sense. Just wondering if there are any ramifications of that. The callback is in C so it shouldn't have issues with this.
A pyperformance suite run on an interpreter modified to support this but having no callbacks registered would be useful. (basically judging if there is measurable overhead added by the watched bit check - I doubt it'll be noticeable in most code)
carljm commentedon Mar 3, 2022
Thanks gps! Working on a PR and will collect pyperformance data as well.
We haven't observed any issues in Cinder with the callback just being called at shutdown, too, but if there are problems with that it should be possible to just have CPython clear the callback at shutdown time.
brandtbucher commentedon Mar 3, 2022
Hm, this is a bit scary. Could we (or others) end up with unguarded stale caches if some buggy extension forgets to chain the calls correctly?
Core CPython seems most at risk of this, since we would most likely be registered first.
brandtbucher commentedon Mar 3, 2022
Also, when you say "only one global callback": does that mean per-interpreter, or per-process?
carljm commentedon Mar 3, 2022
Yes. I can really go either way on this. I initially opted for simplicity in the core support at the cost of asking a bit more of clients, on the theory that a) there are lots of ways for a buggy C extension to cause crashes with bad use of the C API, and b) I don't expect there to be very many extensions using this API. But it's also true that the consequences of a mistake here could be hard to debug (and easily blamed to the wrong place), and there might turn out to be more clients for dict-watching than I expect! If the consensus is to prefer CPython tracking an array of callbacks instead, we can try that.
Good question! The currently proposed API suggests per-process, but it's not a question I've given a lot of thought to yet; open to suggestions. It seems like in general the preference is to avoid global state and instead tie things to an interpreter instance? I'll need to do a bit of research to understand exactly how that would affect the implementation. Doesn't seem like it should be a problem, though it might make the lookup at write time to see if we have a callback a bit slower.
gpshead commentedon Mar 3, 2022
Per interpreter seems best.
If someone using this feature writes a buggy implementation of a callback that doesn't chain reliably, that is a bug in their code and all of the fallout from that is "just" a bug to be fixed in said code.
Think of it like a C signal handler, the OS doesn't chain for you - it is the signal handler installer's responsibility to chain to the previous one. Not an unusual pattern for callback hooks in C.
We already have such global C callback hooks in SetProfile and SetTrace. https://docs.python.org/3/c-api/init.html#profiling-and-tracing
Those don't even provide for chaining. We could do the same here and not let a hook be set more than once instead of providing a Get API to allow for manual chaining. That seems less useful.
markshannon commentedon Mar 4, 2022
Why so coarse?
Getting a notification for every change of a global in module, is likely to make use the use of global variables extremely expensive.
I may well want to be notified if
fooorCONSTgets modified, but performance could suffer badly if we make a callback every timevaris changed.--------------
What happens if a watched dictionary is modified in a callback?
--------------
How do you plan to implement this? Steal a bit from
ma_version_tagor replacema_version_tag?If you replace
ma_version_tagthis could actually speed things up a tad.You'd probably need a PEP to replace PEP-509, but I think this may need a PEP anyway.
brandtbucher commentedon Mar 4, 2022
Perhaps a compromise is possible here: one global group/chain of callbacks registered for all dictionaries in an interpreter seems reasonable (to keep overhead low), but we could reduce the number of times it’s actually called by, say, tagging specific values of specific dictionaries to be watched.
For example, we could just tag the low bit of any pointer in a dictionary’s values that we want to be notified of changes to. Something like that seems like it could really keep the cost of watching down without sacrificing power.
17 remaining items
carljm commentedon Sep 15, 2022
I am on the hook, just not finding time... I definitely plan to make it happen in time for 3.12 though.
pythongh-91052: Add C API for watching dictionaries
GH-91052: Add C API for watching dictionaries (GH-31787)
carljm commentedon Oct 7, 2022
Fixed with merge of #31787
pythongh-91052: Add PyDict_Unwatch for unwatching a dictionary
pythongh-91052: Add PyDict_Unwatch for unwatching a dictionary
gh-91052: Add PyDict_Unwatch for unwatching a dictionary (#98055)
Merge branch 'main' into typewatch
Merge branch 'main' into typewatch
Merge branch 'main' into urllibnonet
pythonGH-91052: Add C API for watching dictionaries (pythonGH-31787)
pythongh-91052: Add PyDict_Unwatch for unwatching a dictionary (pytho…