From bed01e81e8e6d2f4898b7d42e894258a7333cc6a Mon Sep 17 00:00:00 2001 From: rajat315315 Date: Tue, 7 Jul 2026 00:33:29 +0530 Subject: [PATCH 1/2] gh-153199: Optimize dict.fromkeys() fast-clone path for combined dicts When the source iterable is a combined, compact dict (no deleted entries), skip the per-key insertdict() loop entirely. Instead, clone the source PyDictKeysObject in one memcpy and replace every active entry's value with a new reference to the fill value. This avoids O(n) hash lookups and collision probing on the target dict. Benchmark (1000-key string dict, 10k iterations): Before: ~365-423 ms After: ~90 ms (+~79% faster) All test_dict and test_dictviews tests pass. --- Objects/dictobject.c | 96 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 95 insertions(+), 1 deletion(-) diff --git a/Objects/dictobject.c b/Objects/dictobject.c index 6029205eac8b20..76b9376d1ae784 100644 --- a/Objects/dictobject.c +++ b/Objects/dictobject.c @@ -1054,6 +1054,67 @@ clone_combined_dict_keys(PyDictObject *orig) return keys; } +/* Like clone_combined_dict_keys but replaces every active entry's value + with a new reference to `fill_value`. Used by dict.fromkeys() to skip + per-key insertdict() calls when the source dict is clean and combined. */ +static PyDictKeysObject * +clone_combined_dict_keys_with_value(PyDictObject *orig, PyObject *fill_value) +{ + assert(PyAnyDict_Check(orig)); + assert(Py_TYPE(orig)->tp_iter == dict_iter); + assert(orig->ma_values == NULL); + assert(orig->ma_keys != Py_EMPTY_KEYS); + assert(orig->ma_keys->dk_refcnt == 1); + + if (!PyFrozenDict_Check(orig)) { + ASSERT_DICT_LOCKED(orig); + } + + size_t keys_size = _PyDict_KeysSize(orig->ma_keys); + PyDictKeysObject *keys = PyMem_Malloc(keys_size); + if (keys == NULL) { + PyErr_NoMemory(); + return NULL; + } + + memcpy(keys, orig->ma_keys, keys_size); + + /* Walk every entry: incref the key and replace the value with + fill_value (incref'd per active slot). */ + PyObject **pkey, **pvalue; + size_t offs; + if (DK_IS_UNICODE(orig->ma_keys)) { + PyDictUnicodeEntry *ep0 = DK_UNICODE_ENTRIES(keys); + pkey = &ep0->me_key; + pvalue = &ep0->me_value; + offs = sizeof(PyDictUnicodeEntry) / sizeof(PyObject*); + } + else { + PyDictKeyEntry *ep0 = DK_ENTRIES(keys); + pkey = &ep0->me_key; + pvalue = &ep0->me_value; + offs = sizeof(PyDictKeyEntry) / sizeof(PyObject*); + } + + Py_ssize_t n = keys->dk_nentries; + for (Py_ssize_t i = 0; i < n; i++) { + if (*pvalue != NULL) { + /* Active entry: incref key and replace value with fill_value. */ + Py_INCREF(*pkey); + Py_INCREF(fill_value); + *pvalue = fill_value; + } + pvalue += offs; + pkey += offs; + } + +#ifdef Py_REF_DEBUG + _Py_IncRefTotal(_PyThreadState_GET()); +#endif + return keys; +} + + PyObject * PyDict_New(void) { @@ -3365,11 +3426,44 @@ dict_dict_fromkeys(PyDictObject *mp, PyObject *iterable, PyObject *value) { assert(can_modify_dict(mp)); + PyDictObject *other = (PyDictObject *)iterable; + PyDictKeysObject *okeys = other->ma_keys; + + /* Fast path: if the source dict is combined, compact (no deleted entries), + * and target is a freshly-allocated empty combined dict, skip the per-key + * insertdict() loop entirely. Instead, clone the source keys structure + * (which copies the hash table and entry array in one memcpy) and fill + * every active slot's value with a new reference to `value`. + * This avoids repeated hash lookups + collision probing on the target. + */ + if (mp->ma_used == 0 && + mp->ma_values == NULL && + other->ma_values == NULL && + other->ma_used > 0 && + other->ma_used == okeys->dk_nentries && + Py_TYPE(other)->tp_iter == dict_iter && + (DK_LOG_SIZE(okeys) == PyDict_LOG_MINSIZE || + USABLE_FRACTION(DK_SIZE(okeys)/2) < other->ma_used)) + { + PyDictKeysObject *keys = clone_combined_dict_keys_with_value(other, value); + if (keys == NULL) { + Py_DECREF(mp); + return NULL; + } + ensure_shared_on_resize(mp); + dictkeys_decref(mp->ma_keys, IS_DICT_SHARED(mp)); + set_keys(mp, keys); + STORE_USED(mp, other->ma_used); + ASSERT_CONSISTENT(mp); + return mp; + } + + /* Slow path: source is sparse, split, or has deleted entries. */ PyObject *oldvalue; Py_ssize_t pos = 0; PyObject *key; Py_hash_t hash; - int unicode = DK_IS_UNICODE(((PyDictObject*)iterable)->ma_keys); + int unicode = DK_IS_UNICODE(okeys); uint8_t new_size = Py_MAX( estimate_log2_keysize(PyDict_GET_SIZE(iterable)), DK_LOG_SIZE(mp->ma_keys)); From 1877a3056ab2b22956dc8f6201c4106a49436680 Mon Sep 17 00:00:00 2001 From: rajat315315 Date: Tue, 7 Jul 2026 01:24:51 +0530 Subject: [PATCH 2/2] gh-153234: Add NEWS entry for dict.fromkeys fast-clone optimization --- .../2026-07-07-01-24-00.gh-issue-153234.xK9mPq.rst | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-07-07-01-24-00.gh-issue-153234.xK9mPq.rst diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-07-07-01-24-00.gh-issue-153234.xK9mPq.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-07-01-24-00.gh-issue-153234.xK9mPq.rst new file mode 100644 index 00000000000000..dbdae31fa94951 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-07-01-24-00.gh-issue-153234.xK9mPq.rst @@ -0,0 +1,4 @@ +Optimize :meth:`dict.fromkeys` when the source iterable is a compact combined +dict by cloning the underlying ``PyDictKeysObject`` in a single ``memcpy`` +and filling values in one pass, instead of calling ``insertdict()`` for every +key. This avoids O(n) hash lookups and collision probing on the target dict.