PERF: Defer tick materialization during Axes init/clear#31525
PERF: Defer tick materialization during Axes init/clear#31525eendebakpt wants to merge 7 commits intomatplotlib:mainfrom
Conversation
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5708dc8 to
3ac8224
Compare
timhoffm
left a comment
There was a problem hiding this comment.
Thanks for the PR. The speedup is impressive, and the added complexity (rc caching) is bearable.
Strategically, I would like to move away from single-tick handling, but in the mean time this is a reasonable improvement.
Having tick collections is indeed the way to go. This change is orthogonal as it avoids some tick operations altogether. (but maybe if ticks are really fast that would not matter) |
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
a3f1be1 to
d0ed04c
Compare
| # instance._get_tick() can itself try to access the majorTicks | ||
| # attribute (e.g. in certain projection classes which override | ||
| # e.g. get_xaxis_text1_transform). To avoid infinite recursion, | ||
| # bind the attribute to an empty list before calling _get_tick(). | ||
| # _get_tick() may also call reset_ticks(), which pops the attribute | ||
| # from the instance dict; the final setattr below re-binds the | ||
| # (now non-empty) list so subsequent accesses skip the descriptor. | ||
| attr = 'majorTicks' if self._major else 'minorTicks' | ||
| tick_list = [] | ||
| setattr(instance, attr, tick_list) | ||
| # Build the Tick (and its sub-artists) under the rcParams captured | ||
| # at the last Axis.clear() so that a lazily-materialized Tick | ||
| # matches an eager (pre-lazy) Tick (see Axis._tick_rcParams). | ||
| with _rc_context_raw(instance._tick_rcParams): | ||
| tick = instance._get_tick(major=self._major) | ||
| # Re-apply any set_tick_params() overrides to the fresh Tick. | ||
| # Subclasses of Axis (e.g. the SkewXAxis in the skewt gallery | ||
| # example) sometimes override _get_tick() without forwarding | ||
| # _{major,minor}_tick_kw; calling _apply_params() here guarantees | ||
| # those overrides still take effect, matching the pre-lazy | ||
| # behaviour where the first tick was materialized eagerly and | ||
| # updated in place by set_tick_params(). | ||
| tick_kw = (instance._major_tick_kw if self._major | ||
| else instance._minor_tick_kw) | ||
| if tick_kw: | ||
| tick._apply_params(**tick_kw) | ||
| tick._configure_for_axis(instance) | ||
| tick_list.append(tick) | ||
| setattr(instance, attr, tick_list) |
There was a problem hiding this comment.
| # instance._get_tick() can itself try to access the majorTicks | |
| # attribute (e.g. in certain projection classes which override | |
| # e.g. get_xaxis_text1_transform). To avoid infinite recursion, | |
| # bind the attribute to an empty list before calling _get_tick(). | |
| # _get_tick() may also call reset_ticks(), which pops the attribute | |
| # from the instance dict; the final setattr below re-binds the | |
| # (now non-empty) list so subsequent accesses skip the descriptor. | |
| attr = 'majorTicks' if self._major else 'minorTicks' | |
| tick_list = [] | |
| setattr(instance, attr, tick_list) | |
| # Build the Tick (and its sub-artists) under the rcParams captured | |
| # at the last Axis.clear() so that a lazily-materialized Tick | |
| # matches an eager (pre-lazy) Tick (see Axis._tick_rcParams). | |
| with _rc_context_raw(instance._tick_rcParams): | |
| tick = instance._get_tick(major=self._major) | |
| # Re-apply any set_tick_params() overrides to the fresh Tick. | |
| # Subclasses of Axis (e.g. the SkewXAxis in the skewt gallery | |
| # example) sometimes override _get_tick() without forwarding | |
| # _{major,minor}_tick_kw; calling _apply_params() here guarantees | |
| # those overrides still take effect, matching the pre-lazy | |
| # behaviour where the first tick was materialized eagerly and | |
| # updated in place by set_tick_params(). | |
| tick_kw = (instance._major_tick_kw if self._major | |
| else instance._minor_tick_kw) | |
| if tick_kw: | |
| tick._apply_params(**tick_kw) | |
| tick._configure_for_axis(instance) | |
| tick_list.append(tick) | |
| setattr(instance, attr, tick_list) | |
| # tick list materialization logic: | |
| # 1. create a preliminary empty tick list on the instance | |
| # 2. create the first tick via instance._get_tick(). | |
| # - instance._get_tick() can itself try to access the majorTicks attribute | |
| # (e.g. in certain projection classes which override e.g. | |
| # get_xaxis_text1_transform). Therefore step 1 is needed to avoid | |
| # infinite recursion. | |
| # - Creation is done under the rcParams captured at the last Axis.clear() | |
| # so that properties relfect the state of Axis creation/clear. | |
| # 3. apply set_tick_params() and axis config to the tick | |
| # - Subclasses of Axis (e.g. the SkewXAxis in the skewt gallery example) | |
| # sometimes override _get_tick() without forwarding | |
| # _{major,minor}_tick_kw. Calling _apply_params() here guarantees | |
| # those overrides still take effect, matching the pre-lazy | |
| # behaviour where the first tick was materialized eagerly and | |
| # updated in place by set_tick_params(). | |
| # 4. bind the final tick list to the instance | |
| # - _get_tick() may also call reset_ticks(), which pops the attribute | |
| # from the instance dict; the final setattr is the eventual assignment | |
| # so that subsequent accesses skip the descriptor. | |
| attr = 'majorTicks' if self._major else 'minorTicks' | |
| setattr(instance, attr, []) # preliminary empty tick list | |
| with _rc_context_raw(instance._tick_rcParams): | |
| tick = instance._get_tick(major=self._major) | |
| if tick_kw: | |
| tick._apply_params(**tick_kw) | |
| tick._configure_for_axis(instance) | |
| tick_list = [tick] | |
| setattr(instance, attr, tick_list) |
Rewritten to make the complex logic as clear as possible. Motivation
- explain the complete logic first
- structured by main steps and indentend additional explanation
- have the code all together - if you know the background from the comment, seeing and understanding the actual code in one go is easier than distributed commands with long comment in between
Note: I've reverted from having a tick_list all around. I had missed that it has to be re-bound anyway. Under that condition it's IMHO logically simpler to have a temporary (unnamed) empty list, and create and bind the actual list at the end.
| # layout or draw. Spine.__init__ installs self.axes.transData as | ||
| # a placeholder; the real blended transform is set by | ||
| # Spine.set_position via _ensure_position_is_set(). Historically | ||
| # this fired as a side effect of tick materialization during |
There was a problem hiding this comment.
| # this fired as a side effect of tick materialization during | |
| # the spine position was set as a side effect of tick materialization during |
wording
| # layout or draw. Spine.__init__ installs self.axes.transData as | ||
| # a placeholder; the real blended transform is set by |
There was a problem hiding this comment.
This is some spine-internal arcane knowledge. Axes should not need to know about this. Can we add some sort of spine._is_positioned flag?
I'm also not fully following the logic: It seems sping._ensure_position_is_set() does not care about spine._transform, but only checks spine._position is that a shortcoming on _ensure_position_is_set()?
| tick_kw = (instance._major_tick_kw if self._major | ||
| else instance._minor_tick_kw) | ||
| if tick_kw: | ||
| tick._apply_params(**tick_kw) | ||
| tick._configure_for_axis(instance) |
There was a problem hiding this comment.
Optional: We could move the whole tick configuration into the function
| tick_kw = (instance._major_tick_kw if self._major | |
| else instance._minor_tick_kw) | |
| if tick_kw: | |
| tick._apply_params(**tick_kw) | |
| tick._configure_for_axis(instance) | |
| tick._configure_for_axis(instance, self._major) |
PR summary
The performance if matplotlibs ticks is a bottleneck in various plots. See for example the discussions and references in #5665, #31012, #29594.
In this PR we prevent materialization of the
_LazyTickListwhen there are no ticks created yet. With the tick-materialization cascade gone from Axes.__clear, the spine transforms the cascade used to install as a side effect are installed explicitly at the end of __clear.Benchmark results (updated):
Benchmark script
Closes #23771.
AI Disclosure
Claude was used in identifying performance bottlenecks related to tick creation. Initially the goal was to create tick collections (as described in one of the references), but this approach seems to be a small change with large impact.
PR checklist