From 67a52c397b55d9a0b453a5f1c75dc04234e62e83 Mon Sep 17 00:00:00 2001 From: Brittany Reynoso Date: Tue, 25 Aug 2026 20:33:50 -0700 Subject: [PATCH 1/3] Presize sets built from sized iterables set() and set.update() do one big resize up front when the source is another set or a dict, but fall back to incremental growth for every other iterable. Building a set from a list or tuple therefore rebuilds the table log2(n/8) times, re-probing every live entry on each pass. Use the operand's length hint to presize on that path too. Hintless iterables such as generators are unaffected, and a type whose __len__ or __length_hint__ fails is still built exactly as before, just without the presize, so nothing observable changes. The resize policy, which was already spelled out twice, moves into set_presize() so all three call sites share it along with a guard against a __length_hint__ large enough to overflow the arithmetic. | benchmark | before | after | change | |--------------------|----------:|----------:|-------:| | set(list[int]) 1M | 25.76 ms | 21.27 ms | -17% | | set(list[str]) 1M | 97.81 ms | 89.93 ms | -8% | | set(tuple[int]) 1M | 26.51 ms | 20.92 ms | -21% | | frozenset(list) 1M | 30.38 ms | 24.15 ms | -21% | | s.update(list) 1M | 26.66 ms | 20.55 ms | -23% | | set(list[int]) 10k | 132.59 us | 119.74 us | -10% | | set(list[str]) 10k | 179.60 us | 146.24 us | -19% | | set(genexpr) 1M | 40.35 ms | 40.80 ms | +1% | | set(list) 3 elems | 0.10 us | 0.10 us | +1% | | s.update(list) 3 | 0.08 us | 0.08 us | +2% | The last two rows are the cost of the added length-hint lookup, a fixed ~8ns per call on the iterable path. --- Objects/setobject.c | 47 ++++++++++++++++++++++++++++++--------------- 1 file changed, 31 insertions(+), 16 deletions(-) diff --git a/Objects/setobject.c b/Objects/setobject.c index 8fdd1eb26118c0..dd7a21edc9fc75 100644 --- a/Objects/setobject.c +++ b/Objects/setobject.c @@ -562,6 +562,24 @@ set_table_resize(PySetObject *so, Py_ssize_t minused) return 0; } +/* +Do one big resize at the start, rather than incrementally resizing as we insert +new keys. Expect that there will be no (or few) overlapping keys. An `n` too +large for the arithmetic below is ignored, leaving growth incremental. +*/ +static int +set_presize(PySetObject *so, Py_ssize_t n) +{ + assert(n >= 0); + if (n == 0 || n >= PY_SSIZE_T_MAX/8 - so->fill) { + return 0; + } + if ((so->fill + n)*5 >= so->mask*3) { + return set_table_resize(so, (so->used + n)*2); + } + return 0; +} + static int set_contains_entry(PySetObject *so, PyObject *key, Py_hash_t hash) { @@ -842,13 +860,8 @@ set_merge_lock_held(PySetObject *so, PyObject *otherset) if (other == so || other->used == 0) /* a.update(a) or a.update(set()); nothing to do */ return 0; - /* Do one big resize at the start, rather than - * incrementally resizing as we insert new keys. Expect - * that there will be no (or few) overlapping keys. - */ - if ((so->fill + other->used)*5 >= so->mask*3) { - if (set_table_resize(so, (so->used + other->used)*2) != 0) - return -1; + if (set_presize(so, other->used) < 0) { + return -1; } so_entry = so->table; other_entry = other->table; @@ -1195,15 +1208,8 @@ set_update_dict_lock_held(PySetObject *so, PyObject *other) } #endif - /* Do one big resize at the start, rather than - * incrementally resizing as we insert new keys. Expect - * that there will be no (or few) overlapping keys. - */ - Py_ssize_t dictsize = PyDict_GET_SIZE(other); - if ((so->fill + dictsize)*5 >= so->mask*3) { - if (set_table_resize(so, (so->used + dictsize)*2) != 0) { - return -1; - } + if (set_presize(so, PyDict_GET_SIZE(other)) < 0) { + return -1; } Py_ssize_t pos = 0; @@ -1228,6 +1234,15 @@ set_update_iterable_lock_held(PySetObject *so, PyObject *other) return -1; } + Py_ssize_t n = PyObject_LengthHint(other, 0); + if (n < 0) { + PyErr_Clear(); /* advisory only; just grow on demand instead */ + } + else if (set_presize(so, n) < 0) { + Py_DECREF(it); + return -1; + } + PyObject *key; while ((key = PyIter_Next(it)) != NULL) { if (set_add_key(so, key)) { From 4cb48c13cefd935bc96ed7f0b690c742cf4033aa Mon Sep 17 00:00:00 2001 From: Brittany Reynoso Date: Fri, 28 Aug 2026 21:25:35 -0700 Subject: [PATCH 2/3] rework the check + comment --- Objects/setobject.c | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/Objects/setobject.c b/Objects/setobject.c index dd7a21edc9fc75..201da749787263 100644 --- a/Objects/setobject.c +++ b/Objects/setobject.c @@ -563,17 +563,11 @@ set_table_resize(PySetObject *so, Py_ssize_t minused) } /* -Do one big resize at the start, rather than incrementally resizing as we insert -new keys. Expect that there will be no (or few) overlapping keys. An `n` too -large for the arithmetic below is ignored, leaving growth incremental. +Resize at the start of an operation where the final maximum set size is known */ static int set_presize(PySetObject *so, Py_ssize_t n) { - assert(n >= 0); - if (n == 0 || n >= PY_SSIZE_T_MAX/8 - so->fill) { - return 0; - } if ((so->fill + n)*5 >= so->mask*3) { return set_table_resize(so, (so->used + n)*2); } @@ -1236,7 +1230,11 @@ set_update_iterable_lock_held(PySetObject *so, PyObject *other) Py_ssize_t n = PyObject_LengthHint(other, 0); if (n < 0) { - PyErr_Clear(); /* advisory only; just grow on demand instead */ + PyErr_Clear(); /* grow on demand instead */ + } + else if (n == 0 || n >= PY_SSIZE_T_MAX/8 - so->fill) { + /* Either a length hint was not found or the returned value for `n` + could lead to an overflow */ } else if (set_presize(so, n) < 0) { Py_DECREF(it); From 19ac51096e7e4044b5f035c18bb875ca3401c6df Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Sat, 29 Aug 2026 04:42:03 +0000 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../2026-08-29-04-42-02.gh-issue-156548.ywUwcn.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-08-29-04-42-02.gh-issue-156548.ywUwcn.rst diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-08-29-04-42-02.gh-issue-156548.ywUwcn.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-08-29-04-42-02.gh-issue-156548.ywUwcn.rst new file mode 100644 index 00000000000000..3dce3b5f2a237a --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-08-29-04-42-02.gh-issue-156548.ywUwcn.rst @@ -0,0 +1 @@ +Optimize set(), frozenset(), and set.update().