Skip to content

perf(getattr_static): skip exceptions in lookups -- 12% faster test_list - #2127

Closed
dexhunter wants to merge 1 commit into
davidhalter:masterfrom
dexhunter:perf/getattr-static-no-exceptions
Closed

dexhunter wants to merge 1 commit into
davidhalter:masterfrom
dexhunter:perf/getattr-static-no-exceptions

Conversation

@dexhunter

Copy link
Copy Markdown

TL;DR

  • This PR makes getattr_static and its helpers in jedi/inference/compiled/getattr_static.py faster.
  • It skips raised exceptions on common lookups and computes the metaclass __dict__ check once per metaclass.
  • test_list completions benchmark, median of 3 runs: 322.4 ms -> 279.6 ms, about 12.5% faster.
  • Tests: the 5 related test files give 512 passed, 5 skipped, 4 xfailed, the same as master.

What & Why

jedi.Interpreter checks attributes of runtime objects with getattr_static. Each check walks the mro many times. Before this change, the code did extra work on every walk:

  • _is_type raised and caught a TypeError for every non-type object.
  • _check_class and _shadowed_dict raised and caught a KeyError for each mro entry without the attribute.
  • _check_class computed _shadowed_dict(type(entry)) again for every mro entry, also when the metaclass did not change.

Now the code does these checks without exceptions. _is_type uses issubclass(type(obj), type). This is the same check that type.__mro__.__get__ does before it raises. The dict lookups use .get(attr, _sentinel) on the real class mappingproxy. Other mappings keep the old try/except KeyError path. _check_class computes _shadowed_dict once for each new metaclass. _static_getmro reads klass.__mro__ directly when the metaclass is exactly type. The metaclass loop in getattr_static now calls _check_class, because that loop did the same work.

The output stays identical. Each new check returns the same value as the old exception path. Custom metaclasses and shadowed __dict__ still use the old code.

Results

Median time per call, same machine, master against this branch:

Situation Before After Change
test_list sources (list of str/int, 5 completions) 7.91 ms 6.64 ms -16.1%
Interpreter: os.pa on the os module 2.48 ms 1.81 ms -26.9%
Interpreter: __slots__ instance, s. 1.06 ms 0.99 ms -6.0%
Interpreter: Enum class (custom metaclass), Color.RED.na 9.95 ms 9.95 ms 0.0%
Script only, no runtime objects (json.dum) 1.48 ms 1.47 ms -0.4%

The Enum and Script rows show that there is no slowdown where the fast path does not apply.

Why this is safe and easy to maintain

The change is small: about 30 lines in one file, inside the existing private helpers. Each new check gives the same result as the old exception path, so behaviour and output do not change. Classes with custom metaclasses or a shadowed __dict__ still go through the old code. test/test_api/test_interpreter.py covers properties, __getattr__ and side-effect objects. A separate check gave the same completions as master for metaclasses, __slots__ and a shadowed __dict__.

Validation

  • Native tests on this branch:
    pytest test/test_api/test_interpreter.py test/test_inference/test_signature.py test/test_api/test_classes.py test/test_inference/test_imports.py test/test_inference/test_compiled.py
    -> 512 passed, 5 skipped, 4 xfailed (same as master).
  • Full pytest test: 3883 passed. The same 44 tests fail on master and on this branch in my environment (pytest-plugin and namespace-package tests). This change adds no new failures.
  • Benchmark: the 5 test_list sources from test/test_api/test_interpreter.py, run with a fresh jedi.Interpreter per completion. Before 322.4 ms, after 279.6 ms per batch. That is about 12.5% faster (median of 3 runs, each with 15 alternating before/after pairs).
  • Short reproduction:
    import timeit, jedi
    srcs = ["array[0].uppe", "array[0].real", "array[int].real", "array[int()].real", "array[2].upper"]
    ns = {"array": ["haha", 1]}
    print(min(timeit.repeat(lambda: [jedi.Interpreter(s, [ns]).complete() for s in srcs], number=20, repeat=5)))

AI assistance

This change was made with AI tools. Codex (gpt-6.1-sol) generated the candidate changes, and Weco Observe tracked and benchmarked them. A Claude Code agent picked the smallest candidate with a clear speedup, ran the tests and benchmarks above, and wrote this description. No human review happened before this PR.

Weco share link: https://dashboard.weco.ai/share/jz8CGYVULvuxSQYNQI-sEjy_VHNMxTo5

Interpreter completions check runtime attributes with getattr_static,
which walks the mro many times per lookup. Skip the raised TypeError in
_is_type and the KeyError in the class dict lookups, compute the
metaclass _shadowed_dict check once per metaclass, read __mro__ directly
for classes whose metaclass is exactly type, and reuse _check_class for
the metaclass lookup. Custom metaclasses and shadowed __dict__ keep the
old path, so results do not change.

test_list completions benchmark: 322.4 ms -> 279.6 ms (about 12.5%
faster, median of 3 runs).

Co-Authored-By: Aiden <aiden@weco.ai>
@davidhalter

davidhalter commented Nov 2, 2026 •

Copy link
Copy Markdown
Owner

Wow you LLM bros are really really useless. Stop wasting tokens and energy. Your just heating the planet.

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