Skip to content

gh-161242: Use AC vectorcall for AttributeError, ImportError, NameError - #161462

Merged
cmaloney merged 4 commits into
python:mainfrom
cmaloney:ac_vc_exc_kw
Nov 7, 2026
Merged

cmaloney merged 4 commits into
python:mainfrom
cmaloney:ac_vc_exc_kw

Conversation

@cmaloney

@cmaloney cmaloney commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

These exceptions and their subclasses had no vectorcall, and __init__ parsed keywords through PyArg_ParseTupleAndKeywords with a temporary empty tuple on every call. Error messages are unchanged except when more keywords than parameters are given, which now names the unexpected keyword.

The clinic inputs have no docstring, so docstrings are unchanged and these exceptions don't gain a __text_signature__.

pyperf microbenchmark (PGO+LTO, run serially):

Benchmark base vectorcall
AttributeError(s, name=s, obj=s) 127 ns 41.9 ns: 3.02x faster
ImportError(s, name=s, path=s) 129 ns 43.4 ns: 2.96x faster
NameError(s, name=s) 88.3 ns 39.3 ns: 2.24x faster
ModuleNotFoundError(s, name=s) 89.9 ns 40.6 ns: 2.21x faster
AttributeError(s) 39.4 ns 32.1 ns: 1.23x faster
UnboundLocalError(s) 38.6 ns 31.8 ns: 1.22x faster
NameError(s) 38.5 ns 32.2 ns: 1.19x faster
ImportError(s) 40.7 ns 34.4 ns: 1.18x faster
attribute_error_to_none() 176 ns 173 ns: 1.02x faster

Benchmark code:

import pyperf

runner = pyperf.Runner()
setup = """s = 'x'
class O: pass
o = O()
def attribute_error_to_none():
    try:
        o.x
    except AttributeError:
        pass
"""
for stmt in ["AttributeError(s)", "AttributeError(s, name=s, obj=s)",
             "NameError(s)", "NameError(s, name=s)",
             "ImportError(s)", "ImportError(s, name=s, path=s)",
             "ModuleNotFoundError(s, name=s)", "UnboundLocalError(s)",
             "attribute_error_to_none()"]:
    runner.timeit(stmt, stmt, setup)

…ameError

These exceptions and their subclasses had no vectorcall, and `__init__`
parsed keywords through PyArg_ParseTupleAndKeywords with a temporary
empty tuple on every call.  Error messages are unchanged except when more
keywords than parameters are given, which now names the unexpected
keyword.

The clinic inputs have no docstring, so docstrings are unchanged and
these exceptions don't gain a `__text_signature__`.

pyperf microbenchmark (PGO+LTO, run serially):

| Benchmark                        | base    | vectorcall            |
|----------------------------------|:-------:|:---------------------:|
| AttributeError(s, name=s, obj=s) | 127 ns  | 41.9 ns: 3.02x faster |
| ImportError(s, name=s, path=s)   | 129 ns  | 43.4 ns: 2.96x faster |
| NameError(s, name=s)             | 88.3 ns | 39.3 ns: 2.24x faster |
| ModuleNotFoundError(s, name=s)   | 89.9 ns | 40.6 ns: 2.21x faster |
| AttributeError(s)                | 39.4 ns | 32.1 ns: 1.23x faster |
| UnboundLocalError(s)             | 38.6 ns | 31.8 ns: 1.22x faster |
| NameError(s)                     | 38.5 ns | 32.2 ns: 1.19x faster |
| ImportError(s)                   | 40.7 ns | 34.4 ns: 1.18x faster |
| attribute_error_to_none()        | 176 ns  | 173 ns: 1.02x faster  |

Benchmark code:

```python
import pyperf

runner = pyperf.Runner()
setup = """s = 'x'
class O: pass
o = O()
def attribute_error_to_none():
    try:
        o.x
    except AttributeError:
        pass
"""
for stmt in ["AttributeError(s)", "AttributeError(s, name=s, obj=s)",
             "NameError(s)", "NameError(s, name=s)",
             "ImportError(s)", "ImportError(s, name=s, path=s)",
             "ModuleNotFoundError(s, name=s)", "UnboundLocalError(s)",
             "attribute_error_to_none()"]:
    runner.timeit(stmt, stmt, setup)
```

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread Objects/exceptions.c
if (!PyArg_ParseTupleAndKeywords(empty_tuple, kwds, "|$OOO:ImportError", kwlist,
&name, &path, &name_from)) {
Py_DECREF(empty_tuple);
if (BaseException_init((PyObject *)self, args, NULL) == -1) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a behavior change for e = ImportError('a'); e.__init__('b', foo=1). The call to e.__init__ fails both on main and the PR, but with the PR e.args is not modified. I think the change is good and no real issue. Maybe not even add a test for or a note in the news entry.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, I was trying for a while to find a good way to "forward" an args tuple like the original did but it doesn't make a big performance different (< 5 ns) and arguably the new behavior is better.

Comment thread Lib/test/test_call.py Resolved
}
obj = fastargs[1];
skip_optional_kwonly:
__clinic_args = PyTuple_FromArray(args, nargs);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you plan to tackle this in a followup PR right?

@cmaloney cmaloney Nov 7, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I'm trying to keep PRs to just one of

  1. Add a Argument Clinic new code gen + convert minimal example test types to use it
  2. Change existing code gen (which can effect a lot very quickly)
  3. Move things to Argument Clinic and/or Vectorcall. For this I plan to make a type-refactor issue then do a large migration once Generate vectorcall code to parse arguments using Argument Clinic #87613 is done replacing the much slower PyArg_ParseTupleAndKeywords broadly + handwritten ones that are flat for performance.

For this particular piece it's a Clinic codegen optimization that we can avoid a new tuple some of the time. In general I think the goto + local variable structure of a lot of the clinic codegen is a lot more complex than needed, have lots of thoughts but separate feature chain.

@eendebakpt
eendebakpt enabled auto-merge (squash) November 7, 2026 22:43
@eendebakpt
eendebakpt disabled auto-merge November 7, 2026 22:43
@cmaloney
cmaloney merged commit f767423 into python:main Nov 7, 2026
59 of 60 checks passed
@cmaloney
cmaloney deleted the ac_vc_exc_kw branch November 7, 2026 22:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip news type-feature A feature request or enhancement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants