Repository navigation
Conversation
Could you benchmark the case where I'm interested in whether this branch is really necessary. If the performance is comparable, there's no need to keep the existing code path. If there's a significant difference, I'd also like to reconsider the similar code in |
|
Update (2026-11-10): The results below were for the earlier I tried making Some
The difference is small for less sparse cases, but the fallback is noticeably faster once almost all entries have been deleted. Both implementations passed the targeted dict tests, watcher tests, and the sparse-dict stress checks. I used the same Ubuntu 24.04 runner for each comparison, with measurements in A–B–B–A order. The benchmark and workflow are available here: https://github.com/localzet/cpython/actions/runs/42595205328 The GCC PGO/LTO comparison has now completed, and it gave a different result. For example, with The two configurations and raw logs are in the same workflow run linked above. I'll examine |
|
When the number of live entries is extremely small, performance is dominated by the code that skips empty entries. The loop in Lines 3308 to 3310 in d0c69f8 On the other hand, this PR uses the following code: for (Py_ssize_t i = 0; i < oldkeys->dk_nentries; i++) {
if (src[i].me_value != NULL) {If we changed the You're using Codex, right? Codex is good at investigating this kind of low-level implementation detail, so please have it thoroughly investigate whether we really need to introduce another copy path just for this. |
5dae4e0 to
540cab8
Compare
540cab8 to
92f75ad
Compare
|
Thanks for the suggestion. The inner I compared three variants: the original Stopping after I've updated this PR to use the inner Benchmarks: https://github.com/localzet/cpython/actions/runs/42801499895 and https://github.com/localzet/cpython/actions/runs/42803337022 And yes, Codex helped prepare the benchmark variants and tests. I reviewed the changes and checked the results before updating the PR. |
|
I looked for standard-library paths where deletions can leave a sparse dict that is subsequently copied:
I added a standalone benchmark on a separate fork branch (no change to this PR) that exercises these existing methods after constructing dictionaries with many deleted entries. I compared the PR base (
In both standard-library examples, the underlying These are constructed cache states, not measurements of how frequently real applications enter them. They establish concrete call sites where this path is possible, but do not by themselves settle the concern about the maintenance cost of the extra C code. |
|
I ran a follow-up GCC PGO/LTO comparison and instrumented a few official pyperformance workloads to get a better sense of how frequently the new path is used. With PGO/LTO, the speedups in constructed sparse cases are still clear (mean of two medians per variant, same runner, A–B–B–A):
I also added temporary counters to a separate research build of the base commit. They record (a) calls reaching In four completed pyperformance workloads ( Instrumented pyperformance run So the speedup is reproducible when a dict is sparse, but I still don't have evidence that this is common in representative workloads. I understand the concern about adding code to a rarely used path. I'll leave the PR unchanged rather than expand it further without a stronger use case. |
Summary
Copy live entries directly into a compact keys table when copying a combined dictionary, rather than reinserting entries through the generic merge path.
__hash__methods again.whileloop and stop oncema_usedentries have been copied. This avoids scanning trailing tombstones.Tests
Verified on the contributor fork with:
test_dict,test_capi.test_dict,test_capi.test_watchers(251 tests).-R 3:3.detect_leaks=0; heap-leak detection is covered separately by the reference-leak checks).Verification: https://github.com/localzet/cpython/actions/runs/42806793734
Performance
Compared the original version of this PR (the
forloop with a 1/64 density cutoff) with the newwhileloop without the cutoff, on Linux x86-64 with GCC PGO + LTO. Times are ns/op forsource.copy(), averaged across two measurement passes on the same runner, lower is better.There is a small measured slowdown on some denser string-key cases. This is one CI run, not a controlled multi-machine study; the full raw logs include other densities, both key types, and both benchmark passes.
Three-variant benchmark (regular and PGO/LTO): https://github.com/localzet/cpython/actions/runs/42801499895
Deletion pattern benchmark (regular build): https://github.com/localzet/cpython/actions/runs/42803337022
AI assistance was used to explore the implementation and prepare the experimental variants and test automation. The proposed changes were validated with the tests and benchmarks above.