Uh oh!
There was an error while loading. Please reload this page.
gh-153060: Add an empty frozendict singleton - #153061
Conversation
The singleton is created immortal to avoid refcount contention in Free Threading. * Add PyInterpreterState.dict_state.empty_frozendict. * Add _PyDict_Init() and _PyDict_Fini() to create and destroy the empty frozendict singleton.
Examples: >>>empty=frozendict()
>>>frozendict() isemptyTrue>>>frozendict([]) isemptyTrue>>> (empty|empty) isemptyTrue>>> (empty| {}) isemptyTrue>>>frozendict.fromkeys('') isemptyTrue |
vstinner
commented
Jul 5, 2026
cc @corona10 |
corona10
left a comment
There was a problem hiding this comment.
Overall looks good to me, but give me enough time to take a look at see the detail.
I ran a microbenchmark on Linux with CPU isolation on this change:
I'm surprised by the slowdown on these operations:
Details |
ZeroIntensity
left a comment
There was a problem hiding this comment.
I think it would make sense to also add this to Py_GetConstantBorrowed.
| PyMutex watcher_mutex; // Protects the watchers array (free-threaded builds) | ||
| _PyOnceFlag watcher_setup_once; // One-time optimizer watcher setup | ||
| PyDict_WatchCallback watchers[DICT_MAX_WATCHERS]; | ||
| PyObject *empty_frozendict; |
There was a problem hiding this comment.
Singletons are usually supposed to be stored on _PyRuntime.static_objects.singletons, not on the interpreter state. Is there a reason we're deviating from that convention here?
There was a problem hiding this comment.
Adding the empty frozendict singleton to _PyRuntime.static_objects.singletons would require to initialize a PyFrozenDictObject structure statically which is complicated. PyFrozenDictObject inherits from PyDictObject which has these two members:
PyDictKeysObject *ma_keysPyDictValues *ma_values
We should get access to this empty_keys_struct (Py_EMPTY_KEYS) outside dictobject.c.
staticPyDictKeysObjectempty_keys_struct= {
_Py_DICT_IMMORTAL_INITIAL_REFCNT, /* dk_refcnt */0, /* dk_log2_size */3, /* dk_log2_index_bytes */DICT_KEYS_UNICODE, /* dk_kind */#ifdefPy_GIL_DISABLED
{0}, /* dk_mutex */#endif1, /* dk_version */0, /* dk_usable (immutable) */0, /* dk_nentries */
{DKIX_EMPTY, DKIX_EMPTY, DKIX_EMPTY, DKIX_EMPTY,
DKIX_EMPTY, DKIX_EMPTY, DKIX_EMPTY, DKIX_EMPTY}, /* dk_indices */
};Pre-computing hash() is also complicated:
Py_uhash_thash=0;
hash ^= (1) *1927868237UL;
hash ^= (hash >> 11) ^ (hash >> 25);
hash=hash*69069U+907133923UL;
if (hash== (Py_uhash_t)-1) {
hash=590923713UL;
}Well. Adding _PyDict_Init() and _PyDict_Fini() to allocate the empty frozendict singleton using dictobject.c code was simpler for me.
There was a problem hiding this comment.
Couldn't we do something like this?
// singletons ...
.empty_frozendict= {
.ob_base= (PyDictObject){
.ma_used=0,
._ma_watcher_tag=0,
.ma_keys=NULL, // Set in _PyRuntime_Initialize()
.ma_values=NULL// Set in _PyRuntime_Initialize()
},
.ma_hash=-1// Set in _PyRuntime_Initialize()
},You wouldn't have to get the details for empty_keys_struct outside of dictobject.c; I think you could just extern it.
vstinner
commented
Jul 6, 2026
Python had a frozenset singleton, but it was decided to remove it in Python 3.10. frozendict is still very new. I don't know if the empty frozendict singleton will stay forever. If we add So for now, I would prefer prefer to not add it to |
ZeroIntensity
commented
Jul 7, 2026
Why was it removed? I'm fine with not adding one, but I'd like to know the context. |
vstinner
commented
Jul 7, 2026
#21085 removed the empty frozenset singleton in Python 3.10. |
ZeroIntensity
commented
Jul 8, 2026
Hmm, it seems it was removed because nobody was using it. Is there something that makes a Sorry if I'm sounding grouchy -- I'm partially playing devil's advocate here. |
vstinner
commented
Jul 8, 2026
No, I didn't collect any stats about empty frozendict. |
picnixz
commented
Jul 9, 2026
Empty frozendicts: can be used anywhere we use MappingProxyType({}) as a default argument for a mapping parameter. People have module-wide constants say _EMPTY = ... and would use them instead of habing a None default. For frozenset() I am surprised but not that much because it is only used in default arguments when having a recursivenguard for instance. Such guard is rarer when writing C code though however a default frozendict() may be used (I still think it is more likely to use in pure Python rather than in C). |
ZeroIntensity
commented
Jul 9, 2026
I'm fine with this PR then. I'd appreciate investigating whether we can implement this on |
The singleton is created immortal to avoid refcount contention in Free Threading.