Skip to content

Cut per-patch overhead in apply/iapply#55

Merged
Korijn merged 1 commit into
masterfrom
claude/apply-micro
Jul 11, 2026
Merged

Cut per-patch overhead in apply/iapply#55
Korijn merged 1 commit into
masterfrom
claude/apply-micro

Conversation

@Korijn

@Korijn Korijn commented Jul 11, 2026

Copy link
Copy Markdown
Collaborator

Track E, PR 12 — the apply/iapply micro pass.

What

  • Dispatch on the parent's exact class first (dict/list fast paths), falling back to hasattr duck typing only for container look-alikes such as observ proxies — instead of paying up to three hasattr calls on every patch. Duck-typed parents take exactly the same branches as before.
  • Skip the defensive deepcopy for immutable scalar patch values (deepcopy returns the same object for those anyway — semantically identical). Non-scalar values are still copied on write, so the "patches never share mutable state with your objects" guarantee is unchanged, and the docs statement stays true as written.
  • Skip the int() conversion for keys that are already ints (native pointers); the try/except ValueError remains for parsed string tokens.
  • Bind deepcopy and the scalar-type set to locals outside the loop.

Benchmarks

The apply group had one benchmark, dominated by the base-object deep copy, so this PR adds two op-heavy cases (400 mixed dict ops; 100 replaces addressed three levels deep). Interleaved medians vs master, 2 runs each (all numbers still include the base-object copy apply() always pays, so the op-loop itself improved by more):

benchmark master this PR change
apply_dict_many_ops 652.9µs 577.9µs −11.9%
apply_list_1000_elements 254.4µs 227.5µs −10.3%
apply_nested_paths 289.8µs 262.4µs −9.4%

405 tests pass at 100% coverage (property round-trips and the RFC 6902 corpus both exercise this interpreter heavily); ruff and ty clean. Independent of the other Track E PRs — merges in any order.

🤖 Generated with Claude Code

https://claude.ai/code/session_016Mu9vEBwU4fQLi8ZgkgdS2


Generated by Claude Code

Micro pass over the patch interpreter (Track E):

- Dispatch on the parent's exact class first (dict/list fast paths)
  and only fall back to hasattr duck typing for container look-alikes
  such as observ proxies, instead of paying up to three hasattr calls
  on every patch
- Skip the defensive deepcopy for immutable scalar patch values
  (deepcopy returns the same object for those anyway); non-scalar
  values are still copied on write, so patches and patched objects
  never share mutable state
- Skip the int() conversion for keys that are already ints (native
  pointers), keeping the try/except only for parsed string tokens
- Bind deepcopy and the scalar-type set to locals outside the loop

Adds two op-heavy apply benchmarks (400 dict ops; 100 replaces three
levels deep) since the existing one was dominated by the deep copy of
the base object.

Interleaved benchmark medians vs master (2 runs each, including the
base-object copy that apply() always pays):

  apply_dict_many_ops       652.9us -> 577.9us  (-11.9%)
  apply_list_1000_elements  254.4us -> 227.5us  (-10.3%)
  apply_nested_paths        289.8us -> 262.4us  (-9.4%)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Mu9vEBwU4fQLi8ZgkgdS2
@Korijn
Korijn force-pushed the claude/apply-micro branch from e671f80 to b1a135b Compare July 11, 2026 10:37
@Korijn
Korijn merged commit 5edf6de into master Jul 11, 2026
11 checks passed
@Korijn
Korijn deleted the claude/apply-micro branch July 11, 2026 10:44
@Korijn Korijn mentioned this pull request Jul 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants