GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — gh-150815: Speed up copy.deepcopy() of containers with atomic elements by gaborbernat · Pull Request #150822 · python/cpython

Conversation

@gaborbernat

@gaborbernat gaborbernat commented Jun 2, 2026 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Contributor

copy.deepcopy() copies a structure by sending every element back through deepcopy(). For elements that need no copying at all — strings, ints, None, booleans, floats and the other immutable atomic types — that round trip costs a full function call each, even though the value handed back is the same object. Real data is dominated by these atomic leaves: a parsed JSON document, a settings dict cloned before mutation, a record copied inside a framework. The keys are strings and most values are strings and numbers, so copying spends most of its time calling deepcopy() only to get the same object straight back.

This folds the atomic-type check that already gates the top of deepcopy() into the dict, list and tuple copiers, so an atomic element is returned as-is without the per-item call. The check is the same one deepcopy() runs, and atomic objects are not memoized either way, so the result is identical for shared references, recursive structures and int/tuple subclasses.

Deep-copying 105 JSON documents drawn from the top-1000 PyPI projects improves from 1.21 ms to 990 µs, 22% faster. This follows the atomic fast path added in gh-114264, extending it from the entry point to the per-element loop.

Benchmark base patched
deepcopy 105 real corpus JSON objects 1.21 ms 990 µs: 22% faster
Benchmark (pyperf)

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.

  • Issue: Speed up copy.deepcopy() of containers holding atomic elements #150815

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@bedevere-app bedevere-app Bot added the awaiting review label Jun 2, 2026
@bedevere-app bedevere-app Bot mentioned this pull request Jun 2, 2026
Speed up copy.deepcopy() of containers holding atomic elements #150815
Closed
@gaborbernat
gaborbernat force-pushed the opt/deepcopy-inline-atomic branch from aeab6f7 to 8b8b7e8 Compare June 2, 2026 23:45
JelleZijlstra
JelleZijlstra reviewed Jun 3, 2026
View reviewed changes
Comment thread Lib/copy.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

@eendebakpt

eendebakpt commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

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).

Bobronium, gaborbernat, and maurycy reacted with heart emoji
All reactions
  • ❤️ 3 reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@gaborbernat

gaborbernat commented Jun 3, 2026

Copy link
Copy Markdown
Contributor Author

I don't think a pure-Python tweak and a C/Rust deepcopy are opposing directions — they optimize different ends and can coexist. This change helps every build today with no extension to compile and no new maintenance surface, and it doesn't block or complicate a future C implementation; if one lands, this just becomes a small fast path that the C version supersedes.

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 type(...) in _atomic_types check, and the local-variable capture added nothing measurable, so I dropped it.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@gaborbernat
gaborbernat requested review from JelleZijlstra and ZeroIntensity June 3, 2026 14:38
Bobronium
Bobronium reviewed Jun 3, 2026
View reviewed changes
Comment thread Lib/copy.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

gaborbernat added 3 commits June 3, 2026 11:26
@gaborbernat
pythongh-150815: Speed up copy.deepcopy() of containers with atomic e…
0588c61
…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.
@gaborbernat
Use the _atomic_types global directly without capturing it
8968c33
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.
@gaborbernat
Compute element type once in deepcopy container copiers
0c3f9e1
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.
@gaborbernat
gaborbernat force-pushed the opt/deepcopy-inline-atomic branch from 6ceb409 to 0c3f9e1 Compare June 3, 2026 21:58
@gaborbernat

gaborbernat commented Jun 3, 2026

Copy link
Copy Markdown
Contributor Author

@Bobronium is right: the first version ran type() twice for every non-atomic element, once in the inline guard and once again inside deepcopy(). I measured the cost, tested three designs to address it, and picked the one that is fastest with the smallest regression.

Method

I compared each version of Lib/copy.py on the same built interpreter (3.16.0a0), loaded as the copy module, driven by pyperf (1 warmup + 3 values × 20 processes, 5 independent runs per version). Coefficient of variation across the 5 run-means stayed at or below 1.1% on every workload, so the gaps below are real rather than jitter. Workloads span all-atomic to all-non-atomic, with the worst case for the regression (empties: a list of 2000 [], non-atomic elements with no atomic data to amortize the check against).

Options tested

  1. Inline guard (the first push): a if type(a) in _atomic_types else deepcopy(a, memo) in each copier. Skips the call for atomic elements but types non-atomic elements twice.
  2. Inline guard + local capture: same, with type and _atomic_types bound to locals/default args in each copier.
  3. Single type(): split deepcopy() into a thin entry plus an internal _deepcopy_fallback(x, memo, cls) that takes the already-computed class. Each element is typed once whether or not it is atomic.

Results (speedup vs main, higher is better)

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

How I chose

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.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@serhiy-storchaka

serhiy-storchaka commented Jun 7, 2026

Copy link
Copy Markdown
Member

This is a significant code complication. I don't think a 20% gain in microbenchmarks is worth it.

dolfinus reacted with thumbs down emoji
All reactions
  • 👎 1 reaction

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@gaborbernat

gaborbernat commented Jun 7, 2026

Copy link
Copy Markdown
Contributor Author

Considering this is used quite often, the impact can be big. I think a 30-line extra is worth it.

dolfinus reacted with thumbs up emoji
All reactions
  • 👍 1 reaction

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@eendebakpt

eendebakpt commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

I concur with @serhiy-storchaka, this is too much complexity for the gain.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@gaborbernat

gaborbernat commented Aug 23, 2026 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Contributor Author

Alright. 22% for 22 lines IMHO is not too much but I digress.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@gaborbernat gaborbernat closed this Aug 23, 2026
@gaborbernat
gaborbernat deleted the opt/deepcopy-inline-atomic branch November 5, 2026 00:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

@JelleZijlstra JelleZijlstra Awaiting requested review from JelleZijlstra
@ZeroIntensity ZeroIntensity Awaiting requested review from ZeroIntensity
1 more reviewer
@Bobronium Bobronium Bobronium left review comments
Reviewers whose approvals may not affect merge requirements

Uh oh!

There was an error while loading. Please reload this page.

Assignees

No one assigned

Labels

awaiting review

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

Speed up copy.deepcopy() of containers holding atomic elements

Uh oh!

There was an error while loading. Please reload this page.

6 participants

@gaborbernat @eendebakpt @serhiy-storchaka @JelleZijlstra @Bobronium @ZeroIntensity
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.