Thanks to visit codestin.com
Credit goes to github.com

Skip to content

PERF: Cache ticks and ticklabel bboxes within each draw cycle#31012

Open
scottshambaugh wants to merge 8 commits intomatplotlib:mainfrom
scottshambaugh:ticks_cache
Open

PERF: Cache ticks and ticklabel bboxes within each draw cycle#31012
scottshambaugh wants to merge 8 commits intomatplotlib:mainfrom
scottshambaugh:ticks_cache

Conversation

@scottshambaugh
Copy link
Copy Markdown
Contributor

@scottshambaugh scottshambaugh commented Jan 22, 2026

PR summary

Towards #5665

Currently we are calling _update_ticks() 3 times per axis every draw call, and _get_ticklabel_bboxes 2 times per axis. These calls take up about 35% of total draw time for an empty plot.

We can eliminate 25% of total draw time by caching the results of these calculations every draw cycle. This introduces some state, but I think it's decently well guarded and the performance boost is definitely worth it.

Before & After
The circled red areas show the before & after runtime of axis._update_label_position within Axis.draw(). It runs after the cache values have been set by other functions, so we nearly completely eliminate its runtime.

Before:
image

After:
image

Profiling script:

import time
import matplotlib.pyplot as plt

fig, ax = plt.subplots()

print("Timing...")
start_time = time.perf_counter()
for i in range(100):
    fig.canvas.draw()
end_time = time.perf_counter()

plt.close()
print(f"Time taken: {end_time - start_time:.4f} seconds")

PR checklist

@scottshambaugh scottshambaugh marked this pull request as ready for review January 22, 2026 02:32
@scottshambaugh scottshambaugh mentioned this pull request Jan 22, 2026
1 task
@scottshambaugh scottshambaugh changed the title PERF: Cache ticks and tick label bboxes each draw cycle PERF: Cache ticks and ticklabel bboxes within each draw cycle Jan 22, 2026
@timhoffm
Copy link
Copy Markdown
Member

Is this safe? Are you sure they are always identical? For example constrained layout draws twice and the Axes size and limits may have changed in the second draw.

Or asking the other way round: if the updates are redundant, why are we doing them in the first place?

@scottshambaugh
Copy link
Copy Markdown
Contributor Author

scottshambaugh commented Jan 22, 2026

get_tightbbox() which is called during constrained_layout is flagged here to ignore the cache - beyond that I can't find anywhere that we do an update on these values in the middle of a draw cycle (and the tests aren't complaining, though I know that's not fully exculpatory).

As to why we're doing them multiple times in the first place - in 3D we need to access these values in the tick drawing and the grid drawing paths, which might not both be called so both needed to potentially refresh the calcs.

In 2D, I don't see a reason - my hunch it was done without realizing the performance hit of recalculating that state each time.

# axis.draw()
self._clear_ticks_cache()

ticks_to_draw = self._update_ticks(_use_cache=True)
tlb1, tlb2 = self._get_ticklabel_bboxes(ticks_to_draw, renderer, _use_cache=True)

for tick in ticks_to_draw:
    tick.draw(renderer)

self._update_label_position(renderer, _use_cache=True)
self.label.draw(renderer)

self._update_offset_text_position(tlb1, tlb2)
self.offsetText.set_text(self.major.formatter.get_offset())
self.offsetText.draw(renderer)

renderer.close_group(__name__)
self._clear_ticks_cache()

@scottshambaugh
Copy link
Copy Markdown
Contributor Author

scottshambaugh commented Jan 22, 2026

Actually, looking at this again, we can also cache within the get_tightbbox function. I see a similar speedup when profiling with fig.tight_layout() in the inner loop now, and the overall impact is more dramatic since we're doing everything twice.

Before:
image

After:
image

@scottshambaugh
Copy link
Copy Markdown
Contributor Author

Rebased to main

@scottshambaugh
Copy link
Copy Markdown
Contributor Author

This has fallen well off the PRs front page so is probably getting lost, @timhoffm any thoughts?

@timhoffm
Copy link
Copy Markdown
Member

timhoffm commented Mar 27, 2026

I'm uneasy about introducing complex caches. Caching a pure function is fine, but here we have to jump substantial hoops with conditional use of caching and cache invalidation. Touching ~20 code locations for that seems brittle and not something I would want to understand or maintain.

I suspect this need for distributed caching is the result of our bad/fragmented tick handling (and possibly it's even the root cause for the tick inefficienty). I still think something like #5665 (comment) is the prerequisite to get out of the tick mess.

@scottshambaugh
Copy link
Copy Markdown
Contributor Author

scottshambaugh commented Mar 29, 2026

Agree that this is pretty complex state & fragile with respect to future refactors / maintainability. Offsetting that is that ticks are covered by pretty much every single image comparison test so I'm not too worried about regressions. Will leave open for now if anyone else wants to weigh in.

I think the speedup here is significant enough to warrant it, but we did get a good boost on overall ticks performance from #30995 so I'm less gung-ho about pushing this one.

The 3D tick handling is actually a good bit simpler than 2D, so I'll ping you on looking at caching for those when I finish up #30994

@timhoffm
Copy link
Copy Markdown
Member

I'm afraid that the complex cache makes future refactoring like #5665 (comment), that could substantially improve the tick architecture and performance much harder.

How much faster does the test suite get by this?

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants