Skip to content

PERF: Add cache for some speed-up - #20107

Merged
mhvk merged 1 commit into
astropy:mainfrom
mhvk:units-cache-converters
Aug 8, 2026
Merged

mhvk merged 1 commit into
astropy:mainfrom
mhvk:units-cache-converters

Conversation

@mhvk

@mhvk mhvk commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Typically, a code does not that many unit conversions, but often does them multiple times (likely implicitly), so we can speed up things if we cache the converter:

b = np.arange(10.0)
%timeit u.km.to(u.m, b)
on main: 1.24 μs, with cache: 760 ns

q = np.arange(10.0) << u.km
q2 = 1 * u.m
%timeit q + q2
on main: 4.86 μs, with cache: 4.34 μs

q3 = 30*u.deg
%timeit np.sin(q3)
on main: 4.45 μs, with cache: 4.27 μs

Note that these improvements are quiet small, in part because I used relatively simple units for these examples. A more complicated one (on a different machine):

unit = u.Unit("erg/(s.cm2.Hz)"
%timeit unit.to(u.Jy)
on main: 2.5 μs, with cache: 791 ns

p.s. I have not added a changelog entry since the performance increase for the changes here do not quite warrant that. It will become better with the next steps: to replace the lambda with a C version for further speed improvements for small arrays, and then a special numpy scaled dtype for large arrays.

@github-actions

Copy link
Copy Markdown
Contributor

Thank you for your contribution to Astropy! 🌌 This checklist is meant to remind the package maintainers who will review this pull request of some common things to look for.

  • Do the proposed changes actually accomplish desired goals?
  • Do the proposed changes follow the Astropy coding guidelines?
  • Are tests added/updated as required? If so, do they follow the Astropy testing guidelines?
  • Are docs added/updated as required? If so, do they follow the Astropy documentation guidelines?
  • Is rebase and/or squash necessary? If so, please provide the author with appropriate instructions. Also see instructions for rebase and squash.
  • Did the CI pass? If no, are the failures related? If you need to run daily and weekly cron jobs as part of the PR, please apply the "Extra CI" label. Codestyle issues can be fixed by the bot.
  • Is a change log needed? If yes, did the change log check pass? If no, add the "no-changelog-entry-needed" label. If this is a manual backport, use the "skip-changelog-checks" label unless special changelog handling is necessary.
  • Is this a big PR that makes a "What's new?" entry worthwhile and if so, is (1) a "what's new" entry included in this PR and (2) the "whatsnew-needed" label applied?
  • At the time of adding the milestone, if the milestone set requires a backport to release branch(es), apply the appropriate "backport-X.Y.x" label(s) before merge.

@pllim pllim added the benchmark Run benchmarks for a PR label Jul 21, 2026
@pllim

pllim commented Jul 21, 2026

Copy link
Copy Markdown
Member

Is there a need to at least document that there is a cache and if needed, how user can invalidate it?

@nstarman

Copy link
Copy Markdown
Member

Lovely!
Agreed with @pllim, especially since constants can be changed 😬

@mhvk

mhvk commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

Constants cannot be changed after you import units, so that should not be an issue.

But since a unit's hash is by name of the base, it may well be possible to mess oneself up. If so, the speed-up is perhaps not significant enough to worry about. Let me think a bit about it, and maybe switch steps. In the meantime, I'll convert this to draft.

@mhvk
mhvk marked this pull request as draft July 21, 2026 18:45
@pllim

pllim commented Jul 21, 2026

Copy link
Copy Markdown
Member

Given people can define their own units downstream and so on, we should empower them to manage the cache if needed?

@mhvk

mhvk commented Jul 21, 2026 •

Copy link
Copy Markdown
Contributor Author

I fear we might need to cache by unit id, which would work since units are immutable. Or possibly weak references, with a WeakKeyDictionary (keyed by other on self). Anyway, needs experimenting... (EDIT: in particular to verify there still is a performance benefit...)

@mhvk

mhvk commented Aug 8, 2026

Copy link
Copy Markdown
Contributor Author

@pllim, @nstarman - I thought a bit more about possible problems with the cache, and really it boils down to whether one can ever get the same hash for two units even though their actual physical units are not the same. As I mentioned above, changing constants is not a possible route: after importing astropy.units, one is stuck with whatever is there.
However, in current master, it is possible create two new named units with the same name but representing something different. The problem for that case is that our hash algorithm assumes names are unique the moment a unit has entered a CompositeUnit. If instead of names I hash based on the underlying units, the problem goes away (see #20224).

If we agree that is a good route, then the caching here should be safe; I'll rebase once we have reached a decision on #20224.

Typically, a code does not that many unit conversions, but often does
them multiple times (likely implicitly), so we can speed up things if
we cache the converter:

```
b = np.arange(10.0)
%timeit u.km.to(u.m, b)
on main: 1.24 μs, with cache: 760 ns

q = np.arange(10.0) << u.km
q2 = 1 * u.m
%timeit q + q2
on main: 4.86 μs, with cache: 4.34 μs

q3 = 30*u.deg
%timeit np.sin(q3)
on main: 4.45 μs, with cache: 4.27 μs
```

Note that these improvements are quiet small, in part because I used
relatively simple units for these examples.  A more complicated one
(on a different machine):
```
unit = u.Unit("erg/(s.cm2.Hz)"
%timeit unit.to(u.Jy)
on main: 2.5 μs, with cache: 791 ns
```
@mhvk
mhvk force-pushed the units-cache-converters branch from 037bba3 to c46f63f Compare August 8, 2026 15:50
@mhvk
mhvk marked this pull request as ready for review August 8, 2026 15:51
Comment thread astropy/units/core.py
converter = unit_scale_converter
else:
return lambda val: scale * _condition_arg(val)
converter = lambda val: scale * _condition_arg(val)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It would be nice to be able to use the new Scaler here, but _condition_arg prevents it. There's no good way around this, like lowering _condition_arg into C and adding it as an option to Scaler?

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.

No, it doesn't prevent it: _condition_arg is built into Scaler! This PR is meant as a prelude: once in, I'll rebase #20183 on top. A nice extra is that then the fact that Scaler caches some object construction inside will become useful, since the scaler instance will remain in the converter dict and thus live for a whole session.

@mhvk
mhvk merged commit 1fdfae1 into astropy:main Aug 8, 2026
37 of 38 checks passed
@mhvk
mhvk deleted the units-cache-converters branch August 24, 2026 14:00
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.

3 participants