Reduce TypeForm recognition slowdown (Take 3) - #21833
Conversation
…-type in try_parse_as_type_expression() Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…n frequency Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ry_parse_as_type_expression() Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e_as_type_expression() Implemented with plain string operations rather than a regular expression. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…omponent in try_parse_as_type_expression() Implemented with plain string operations rather than a regular expression. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This comment has been minimized.
This comment has been minimized.
ilevkivskyi
left a comment
There was a problem hiding this comment.
This looks better, thanks! I have couple comments/questions, plus also
The same skip cannot be used in the semantic analyzer pass's function
because the type context is not yet known.
This is not true. Some type context is known very early. And I think in reality such situations are quite common, since many types are declared, not inferred.
|
@davidfstr Not sure what is the situation here. Are you expecting me/us to just merge the code the logic of which no one (not even you, the author) understand? Or are you going to do some more work here? |
|
Let's consider the comment threads that are open:
This comment appears to be asking for further optimization work. In particular I don't see a correctness concern. My high level response is: "Isn't this PR providing enough of a speedup?"
I will spend some additional time to take the empirical measurements needed to answer this question, in the next few days. |
|
Sorry I've been curt. Many things this week (unrelated to this thread/PR) have been frustrating me and I see it's starting to bleed into my communications elsewhere. |
Speed up is OK, I am worried about merging code logic/motivation behind which I don't understand. I know there is some widespread opinion that nobody really knows (or even cares) how anything works. And although it may be true for 99% of all software, it is not true for mypy, there are people who know exactly how it works. And I want it to stay this way :-) So, if possible, please spend some time investigating my questions/comments, and either adjust the code, or add some clear (but concise) comments explaining the motivation. |
…tmost component in try_parse_as_type_expression()
Drop unreachable condition `self.var_is_typing_special_form(node)`.
A dotted name like `Self.foo`, where the leftmost part is a special form,
is never a valid type.
Verified as unreachable empirically on codebases
{mypy, sphinx, scrapy, pylint}.
… leftmost
The leftmost component of a dotted IndexExpr base is a module or class
prefix, so it can never itself be a typing special form.
Measured over {mypy, sphinx, scrapy, pylint}:
1,912 Var lookups at this site, 0 of which var_is_typing_special_form() rescued.
This comment has been minimized.
This comment has been minimized.
Previously the following assignments were disallowed improperly
on this branch (but not on main):
typx: TypeForm
typx = 'Never'
typx = 'NoReturn'
…e kind of value ...and not some kind of type expression
|
OK. I've reloaded this code into my head and made some improvements at the 2 sites that had comments.
FWIW, I agree that this is a completely valid concern, and that you were right to push back. |
This comment has been minimized.
This comment has been minimized.
|
Also, I can rerun the profiling numbers in the PR description, now that additional changes have been made on this branch. Let me know. |
ilevkivskyi
left a comment
There was a problem hiding this comment.
Thanks! This looks much better. I have one more minor comment. Also could you please refactor most of the block under elif isinstance(maybe_type_expr, StrExpr) into a separate method like string_can_be_type_expression()?
Will do |
…fragile allowlist Remove outdated comment
|
All outstanding feedback applied. I think extracting the now-more-complex string logic to its own function is a notable improvement. The left-over |
|
According to mypy_primer, this change doesn't affect type check results on a corpus of open source code. ✅ |
|
@ilevkivskyi Thank you for the several rounds of feedback 👍🏻 I’m glad to close out this optimization thread. It took longer to pin down the details than I expected. But with persistent effort, we managed it. |
References #21262. Replaces #21585 and obsoletes #21596.
Summary
Enabling
TypeFormby default (referenced #21262) madeSemanticAnalyzer.try_parse_as_type_expressionrun eagerly on every expressionin certain syntactic positions. The cost is concentrated in the expensive
full-parse block (
expr_to_analyzed_type+isolated_error_analysis), whichfails ~87% of the time - pure wasted work.
This branch adds early-reject filters that eliminate 74% of full parses
(2570 → 666) on mypy's self-check, recovering ~46% of the regression:
+1.57% → +0.84% CPU time.
No new regexes - per review feedback on replaced #21585. Every filter here
is plain string/
isinstancework, and the two shape tests that were regexesare now helper functions.
Why not do a type-context check?
Review of replaced #21585 suggested skipping the call to
SemanticAnalyzer.try_parse_as_type_expressionentirely when the type contextcannot be a
TypeForm. That optimization already exists, but in the othertype checker pass at
ExpressionChecker.try_parse_as_type_expression.The same skip cannot be used in the semantic analyzer pass's function
because the type context is not yet known.
So cheaply filtering the inputs to
SA.try_parse_as_type_expressionis theonly remaining (obvious) lever to reduce its runtime contribution.
Optimization Results
CPU time, single worker, paired per-round deltas, n=300:
<TypeForm-disabled-commit>5bb72b788<tip-of-this-pr-branch>The feature branch recovers
19.5 ms of the 41.9 ms regression (~46% by paired median) -
leaving +22.4 ms (~54%). Derivation:
A separate 2-way run of master vs
<tip-of-this-pr-branch>measured−21.1 ms ±3.2, consistent with the 19.5 ms recovered that was derived above.
Notes on the measurement
The baseline (
<TypeForm-disabled-commit>) is current master (5bb72b788)with referenced #21262 (SHA:
dd851f559) reverted, so all three arms sharetoday's code and differ only in
TypeForm. Measuring against the originalpre-#21262 master commit instead of today's master would have conflated
optimizations made during the following ~80 commits, including notably
c0cced35c,which optimised
SA.try_parse_as_type_expressionspecifically.Thus runtime regression measured here (+41.9 ms) is smaller than the
+50.2 ms reported in replaced #21585: part of the original regression has
already been absorbed upstream.
Full parses per self-check, identical corpus:
The successful-parse count is unchanged at every commit on the branch,
as expected: No expression that previously parsed as a type stopped doing so.
Overview of changes
SemanticAnalyzer.try_parse_as_type_expressionfunction. All other changes occur within the same file.
The filter commits
Bare-identifier strings (
"Foo"):Varwhose declared type is a concreteInstance- a value, not a type.FuncDef/OverloadedFuncDef/MypyFile- functions and modules are never types.Other strings:
in a type expression - leading/trailing
., or one of!:/<>@%$^?;&~`\,or a
-that is not aLiteral[...]unary minus. Catches"utf-8",".pyi","error:","pkg/mod.py"."builtins.tuple","typing.Mapping"): look up theleftmost component and reject when it does not resolve, or resolves to a
placeholder or a value
Var.Filters 4 and 5 replace
_NONTYPE_PATTERN_REand_DOTTED_IDENTIFIER_REfromreplaced #21585 with the helpers
has_nontype_char()anddotted_identifier_leftmost().Each was verified to agree with the regex it replaces on all 1171 distinct
strings the full-parse profiler observes during a self-check.
Two specific hazards, and how they are handled
var_is_typing_special_formwas extended to recognizetyping.Self/typing_extensions.Self, so filter 1 does not reject a stringified'Self'annotation (otherwise
testSelfRecognizedInOtherSyntacticLocationsregresses).In filter 4,
-is treated as a unary minus wherever the preceding non-spacecharacter is
[or,, so"Literal[-1, -2]"and"Literal[1, -2]"are stillrecognized. (
_NONTYPE_PATTERN_REin replaced #21585 used(?<!\[)-, which rejectedthose.) On the strings observed during a self-check the two rules reject
identical sets, so the (improved) soundness costs nothing.
Notes
I don't think it's worth trying to recover the remaining +22.4 ms:
no common cheap/obvious shape left
OpExprfilters that actuallygave a net slowdown of 1.9ms.
The profiling instrumentation and the
misc/perf_compare.pyimprovements used to produce these numbers are in a separate PR: Enhance/extend general & TypeForm-specific performance instrumentation #21832. Happy to fold them in here instead if that is easier to review.