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() is empty
True
>>> frozendict([]) is empty
True
>>> (empty | empty) is empty
True
>>> (empty | {}) is empty
True
>>> frozendict.fromkeys('') is empty
True |
|
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.
static PyDictKeysObject empty_keys_struct = {
_Py_DICT_IMMORTAL_INITIAL_REFCNT, /* dk_refcnt */
0, /* dk_log2_size */
3, /* dk_log2_index_bytes */
DICT_KEYS_UNICODE, /* dk_kind */
#ifdef Py_GIL_DISABLED
{0}, /* dk_mutex */
#endif
1, /* 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_t hash = 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.
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 |
The singleton is created immortal to avoid refcount contention in Free Threading.