Skip to content

perf: build error-context strings only when an exception is decorated - #4331

Open
ikrommyd wants to merge 6 commits into
scikit-hep:mainfrom
ikrommyd:perf-improve-error-contexts
Open

ikrommyd wants to merge 6 commits into
scikit-hep:mainfrom
ikrommyd:perf-improve-error-contexts

Conversation

@ikrommyd

@ikrommyd ikrommyd commented Sep 12, 2026 •

Copy link
Copy Markdown
Member

Every public call builds an ErrorContext so that an exception raised inside it can be decorated with the call and its arguments, but the exception almost never happens. The contexts now keep their arguments and format them only when the note is actually read, which also makes PartialFunction and WeakMethodProxy unnecessary: they existed to defer the formatting while keeping the context weakly referenced, and with the arguments held directly there is no cycle to avoid.

The one case that still formats eagerly is a delayed (cuda) backend, because its errors surface later at synchronization from a context that is kept alive until then, and that context must not pin its arguments in the meantime. Those contexts drop the arguments as soon as they have the strings.

Exception types, messages and notes are unchanged; I diffed them against main over the error paths I could think of (bad field, bad axis, jagged slice mismatch, ufunc broadcast failures, nested operations and slices, typetracer, threads, and cuda errors surfaced through synchronize_cuda).

Saves roughly a microsecond per call, which is 4-10% on cheap operations like ak.num or arr[0].

@github-actions github-actions Bot added the type/perf PR title type: perf (set automatically) label Sep 12, 2026
@codecov

codecov Bot commented Sep 12, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 83.87%. Comparing base (60390ac) to head (633cf20).

Additional details and impacted files
Files with missing lines Coverage Δ
src/awkward/_errors.py 83.48% <100.00%> (-1.11%) ⬇️

... and 1 file with indirect coverage changes

github-actions Bot added a commit that referenced this pull request Sep 13, 2026
@github-actions

Copy link
Copy Markdown
Contributor

The documentation preview is ready to be viewed at https://awkward-array.org/doc/pr/4331/

@ikrommyd

ikrommyd commented Sep 13, 2026 •

Copy link
Copy Markdown
Member Author

I need to think about this a little more but this has been discussed as a problem in the past where the error context is built anyways for no reason. Keeping it in draft for a bit

@ikrommyd
ikrommyd marked this pull request as ready for review September 13, 2026 14:10
@ikrommyd

Copy link
Copy Markdown
Member Author

🤖 AI text below 🤖

CPU timings, main vs this branch, median of 7 repeats per case.

case base (us) branch (us) change
a[1:-1] [small n=10] 13.27 12.10 -8.8%
a[1:-1] [large n=1000000] 13.35 12.31 -7.8%
ak.concatenate([a, a]) [large n=1000000] 9,292.8 8,574.5 -7.7%
a[idx] [small n=10] 32.04 29.94 -6.6%
a[idx] [large n=1000000] 1,243.7 1,239.7 -0.3%

Median across all cases: -2.2% (12 cases total, showing the four best and the worst).

benchmark script
"""Benchmark for PR #4331: build error-context strings only when decorating.

Every `ak.*` operation enters an OperationErrorContext and every `__getitem__`
enters a SlicingErrorContext, so the cost measured here is the per-call
book-keeping those context managers add to ordinary, non-raising user code.
"""

import statistics
import timeit

import numpy as np

import awkward as ak


def report(label, stmt, glb, *, number, repeats=7):
    ts = timeit.repeat(stmt, number=number, repeat=repeats, globals=glb)
    us = sorted(t / number * 1e6 for t in ts)
    print(
        f"{label:<45} {statistics.median(us):10.3f} us  min {us[0]:10.3f}  sd {statistics.stdev(us):8.3f}  (n={number}x{repeats})"
    )


print(f"awkward {ak.__version__} from {ak.__file__}")

# 6 statements x 2 sizes = 12 benchmarks.
STATEMENTS = [
    "ak.num(a, axis=1)",  # one array arg + one kwarg
    "ak.sum(a, axis=1)",
    "ak.zip({'x': a, 'y': a})",  # dict arg (no backend of its own)
    "ak.concatenate([a, a])",  # list arg -> recursive backend probe
    "a[1:-1]",  # SlicingErrorContext, plain slice
    "a[idx]",  # SlicingErrorContext, array slice
]

for size, tag, number in ((10, "small", 20_000), (1_000_000, "large", 100)):
    flat = np.arange(3 * size, dtype=np.int64)
    array = ak.unflatten(flat, np.full(size, 3, dtype=np.int64))
    index = np.arange(1, size, dtype=np.int64)
    glb = {"ak": ak, "a": array, "idx": index}
    for statement in STATEMENTS:
        report(f"{statement}  [{tag} n={size}]", statement, glb, number=number)

github-actions Bot added a commit that referenced this pull request Sep 14, 2026
github-actions Bot added a commit that referenced this pull request Sep 15, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type/perf PR title type: perf (set automatically)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant