From b1a135bb127b82b86e2b0ebd2489277a1de8cebd Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 11 Jul 2026 08:15:34 +0000 Subject: [PATCH] Cut per-patch overhead in apply/iapply 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 Claude-Session: https://claude.ai/code/session_016Mu9vEBwU4fQLi8ZgkgdS2 --- benchmarks/benchmark.py | 30 ++++++++++++++++++++++++++++++ patchdiff/apply.py | 27 ++++++++++++++++++++------- 2 files changed, 50 insertions(+), 7 deletions(-) diff --git a/benchmarks/benchmark.py b/benchmarks/benchmark.py index 2ef9111..7fd1175 100644 --- a/benchmarks/benchmark.py +++ b/benchmarks/benchmark.py @@ -264,6 +264,36 @@ def test_apply_list_1000_elements(benchmark): benchmark(apply, a, ops) +@pytest.mark.benchmark(group="apply") +def test_apply_dict_many_ops(benchmark): + """Benchmark: apply a 400-op patch (replaces, adds, removes) to a + 1000-key dict — the op-loop dominates over the deep copy.""" + a = {f"key_{i}": i for i in range(1000)} + b = dict(a) + for i in range(200): + b[f"key_{i}"] = i + 10000 + for i in range(100): + b[f"new_key_{i}"] = i + for i in range(100): + del b[f"key_{i + 200}"] + ops, _ = diff(a, b) + + benchmark(apply, a, ops) + + +@pytest.mark.benchmark(group="apply") +def test_apply_nested_paths(benchmark): + """Benchmark: apply 100 replaces addressed three levels deep, so + pointer evaluation is a significant share of the work.""" + base = {"items": [{"id": i, "meta": {"prio": i % 3}} for i in range(100)]} + changed = copy.deepcopy(base) + for item in changed["items"]: + item["meta"]["prio"] = 9 + ops, _ = diff(base, changed) + + benchmark(apply, base, ops) + + # ======================================== # Pointer Evaluate Benchmarks # ======================================== diff --git a/patchdiff/apply.py b/patchdiff/apply.py index aa67737..244f41e 100644 --- a/patchdiff/apply.py +++ b/patchdiff/apply.py @@ -6,6 +6,10 @@ from .pointer import Pointer from .types import Diffable, Operation +# Immutable types that can never contain (or be) shared mutable state, so +# patch values of these types can be written without a defensive copy. +_SCALAR_TYPES = frozenset({int, float, complex, bool, str, bytes, type(None)}) + def iapply(obj: Diffable, patches: list[Operation]) -> Diffable: """Apply a list of patches to an object, in place. @@ -24,6 +28,8 @@ def iapply(obj: Diffable, patches: list[Operation]) -> Diffable: """ if not patches: return obj + scalar_types = _SCALAR_TYPES + copy_value = deepcopy for patch in patches: # The interpreter below is duck-typed on purpose (dict/list/set # look-alikes such as observ proxies must work), so the operation @@ -36,17 +42,24 @@ def iapply(obj: Diffable, patches: list[Operation]) -> Diffable: key: Any = target[1] value: Any = None if op != "remove": - value = deepcopy(op_dict["value"]) - if hasattr(parent, "keys"): # dict + value = op_dict["value"] + if value.__class__ not in scalar_types: + value = copy_value(value) + # Dispatch on the parent's exact class first (the overwhelmingly + # common case), falling back to duck typing for container + # look-alikes such as observ proxies. + parent_cls = parent.__class__ + if parent_cls is dict or (parent_cls is not list and hasattr(parent, "keys")): if op == "remove": del parent[key] else: # add/replace parent[key] = value - elif hasattr(parent, "append"): # list - try: - key = int(key) - except ValueError: - pass + elif parent_cls is list or hasattr(parent, "append"): + if key.__class__ is not int: + try: + key = int(key) + except ValueError: + pass if op == "replace": parent[key] = value