GeezerVisonThe World Wide Web, restored to 800 pixels of glory. | |||||||||||||||||||||||||||||||||||||||||||
| Home|About|Gaming cabinet|Archives|How it works|Starter sites | |||||||||||||||||||||||||||||||||||||||||||
GeezerVison is showing github.com — gh-150815: Speed up copy.deepcopy() of containers with atomic elements by gaborbernat · Pull Request #150822 · python/cpython Conversation
|
| Benchmark | base | patched |
|---|---|---|
| deepcopy 105 real corpus JSON objects | 1.21 ms | 990 µs: 22% faster |
Run base vs patched by swapping Lib/copy.py on the same interpreter. The figure above is from 105 JSON documents in the top-1000 PyPI corpus; the self-contained script below builds an equivalent atomic-heavy structure and shows a comparable percentage gain.
import copy, pyperf # Representative of parsed-JSON / config data: string keys, scalar leaves. doc = { "name": "example-package", "version": "1.2.3", "private": False, "scripts": {"build": "tsc", "test": "pytest", "lint": "ruff check ."}, "keywords": ["cli", "async", "http", "json"], "dependencies": {f"dep{i}": f"^{i}.0.0" for i in range(20)}, "authors": [{"name": f"Person {i}", "email": f"p{i}@example.com", "active": True} for i in range(10)], "config": {"timeout": 30, "retries": 3, "verbose": False, "level": None}, } objs = [doc] * 50 runner = pyperf.Runner() runner.bench_func("deepcopy atomic-heavy structures", lambda: [copy.deepcopy(o) for o in objs])
Resolves #150815.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
aeab6f7 to
8b8b7e8
Compare
There was an error while loading. Please reload this page.
|
There are more options available to make deepcopy faster (see #91610 (comment)). If we want to make deepcopy faster, I believe we should gather enough support so a core dev can review a C (or rust?) implementation. With a C implementation we have much larger performance gains (see https://github.com/percolab/copium for example). |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
I don't think a pure-Python tweak and a C/Rust I've also reduced this PR to the minimal change: a 3-way benchmark (base / inline-check-with-local-capture / inline-check-using-the-global) showed the entire speedup comes from the inlined |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
…lements The dict, list and tuple deep-copiers send every element back through deepcopy(), paying a function call even for atomic immutable elements that deepcopy() returns unchanged. Inline the atomic-type check into the three copiers so those elements are returned as-is. Behavior is identical, including shared references, recursion and int/tuple subclasses.
The benchmark shows the speedup comes entirely from the inlined type(...) in _atomic_types check, not from binding the global to a local default argument, so drop the local capture for a minimal change.
Address review: the inline atomic check recomputed type() for non-atomic elements (once inline, once inside deepcopy()), a measurable regression on non-atomic-heavy containers. Split deepcopy() into a thin entry plus _deepcopy_fallback() that takes the already-computed class, so each element is typed exactly once whether or not it is atomic. Apply the same pattern to the list, tuple, dict and frozendict copiers.
6ceb409 to
0c3f9e1
Compare
|
@Bobronium is right: the first version ran MethodI compared each version of Options tested
Results (speedup vs
|
| Benchmark | base | 1. inline | 2. inline+local | 3. single-type |
|---|---|---|---|---|
| json_corpus (atomic-heavy) | 1.777 ms | 1.34x | 1.39x | 1.40x |
| list[int] (pure atomic) | 0.131 ms | 2.11x | 2.27x | 2.19x |
| empties (worst non-atomic) | 1.804 ms | 0.96x | 0.96x | 0.98x |
| nested dict (realistic) | 8.817 ms | 1.13x | 1.15x | 1.18x |
| list[Obj] (non-atomic obj) | 6.470 ms | 1.05x | 1.07x | 1.06x |
Option 2 (local capture) buys about 3% on the atomic cases but leaves the worst case at 0.96x, since it removes a global lookup, not the second type() call. It does not address the review. (I also tried option 3 plus local capture; it came out slightly worse than plain option 3 on the worst case and the realistic case, so the extra binding was not worth it.)
Option 3 wins on every axis that matters: it has the smallest regression (0.98x vs 0.96x, halving the worst-case loss), it is fastest on the realistic mixed workload (+18%), and it keeps the atomic gains (1.40x and 2.19x). The regression that remains is narrow: it needs non-atomic elements holding no atomic data anywhere in their subtree. Any atomic leaf tips the structure positive, which is why nested dict lands at +18%.
The branch now uses option 3, applied to the list, tuple, dict and frozendict copiers. test_copy (83 tests) and test_pickle pass. Raw pyperf JSON for all runs is available if anyone wants to verify.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
This is a significant code complication. I don't think a 20% gain in microbenchmarks is worth it. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
Considering this is used quite often, the impact can be big. I think a 30-line extra is worth it. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
I concur with @serhiy-storchaka, this is too much complexity for the gain. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
|
Alright. 22% for 22 lines IMHO is not too much but I digress. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
Successfully merging this pull request may close these issues.
Speed up copy.deepcopy() of containers holding atomic elements
There was an error while loading. Please reload this page.