Skip to content

Grow VOTable result arrays in place instead of copying them - #20743

Merged
pllim merged 2 commits into
astropy:mainfrom
jdavies-st:votable-grow-arrays-in-place
Nov 11, 2026
Merged

pllim merged 2 commits into
astropy:mainfrom
jdavies-st:votable-grow-arrays-in-place

Conversation

@jdavies-st

@jdavies-st jdavies-st commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Description

This PR makes VOTable reading faster, and it uses less peak memory.

Reading a VOTable used to copy the whole table every time the result arrays filled up, and once more at the end to trim them. This was required because masked arrays were used and cannot grow in-place. This PR keeps the values and the missing-value mask in two plain numpy arrays that can be grown and trimmed in place, and builds the masked array once at the end.

Funny enough, this is not new. The reader worked this way until 2012, when 13d7400 put the values and the mask into a single masked array for a simpler interface (with the Table class), and growing the table began to copy it. That interface does not change here; only the reading goes back to two arrays. It is faster and more memory efficient to do so.

Reading the 2.5 GB Gaia table from #8946 (with #20509 merged) now peaks at 5.1 GB memory usage instead of 6.8 GB and takes 113s instead of 133s. That's a 25% improvement in peak memory usage (up the size of the table) and a 15% speedup.

The table that comes back is unchanged: same type, dtype, mask, fill value and values. A new test reads 1000 rows in each of TABLEDATA, BINARY and BINARY2, which grows the arrays past the first 512 rows and then trims them.

The downstream pyvo and astroquery test suites pass as well (locally), using the tox jobs from #20604.

AI Disclosure

I used Claude Opus 5.5 to identify where there was still unneeded memory usage in the VOTable reader, and to help draft the changes here.

  • I certify that I am human and that I take full responsibility for this pull request including all interactions with reviewers.

Merge method

  • By checking this box, the PR author has requested that maintainers do NOT use the "Squash and Merge" button. Maintainers should respect this when possible; however, the final decision is at the discretion of the maintainer that merges the PR.

@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.

@jdavies-st
jdavies-st force-pushed the votable-grow-arrays-in-place branch from baaee44 to 0e75634 Compare October 30, 2026 11:35
@pllim pllim added this to the v8.1.0 milestone Oct 30, 2026
@pllim pllim added the benchmark Run benchmarks for a PR label Oct 30, 2026
@pllim
pllim requested a review from stvoutsin October 30, 2026 14:41
@pllim

pllim commented Oct 30, 2026

Copy link
Copy Markdown
Member

👏

Change Before [d3ad3b6] After [31045de] Ratio Benchmark (Parameter)
- 1.19±0.01s 908±4ms 0.76 votable.TimeVOTableShortStrings.time_short_strings_binary
- 1.32±0.01s 990±5ms 0.75 votable.TimeVOTableLongStrings.time_long_strings_binary2
- 1.02±0.02s 742±7ms 0.73 votable.TimeVOTableBooleanFields.time_booleans_binary
- 5.95±0.05ms 4.28±0.1ms 0.72 votable.TimeVOTableSmallOverhead.time_small_binary2
- 823±7ms 570±3ms 0.69 votable.TimeVOTableLongStrings.time_long_strings_binary
- 920±5ms 635±5ms 0.69 votable.TimeVOTableNumeric.time_numeric_binary
- 3.72±0.1ms 2.09±0.1ms 0.56 votable.TimeVOTableSmallOverhead.time_small_binary

SOME BENCHMARKS HAVE CHANGED SIGNIFICANTLY.
PERFORMANCE INCREASED.

@jdavies-st

Copy link
Copy Markdown
Contributor Author

Oh, those are better than I expected on speed!

I started this branch only worrying about the memory of that big Gaia table, but the speedups are nice, and the benchmarked small tables have modest memory gains, up to the size of the table.

@stvoutsin stvoutsin left a comment

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.

Yea this makes total sense, we don't really need the masked array until the end.
A few comments added here, but no show-stoppers.

Comment thread astropy/io/votable/tree.py Resolved
Comment thread astropy/io/votable/tests/test_tree.py Resolved
Comment thread astropy/io/votable/tree.py Resolved
Comment thread astropy/io/votable/tree.py Resolved
Comment thread astropy/io/votable/tree.py Resolved
@jdavies-st

Copy link
Copy Markdown
Contributor Author

One general thing I'll add is that numpy is considering deprecating .resize. See

numpy/numpy#28800 (comment)

though there is some pushback for precisely our use case, and that it is also used internally within numpy. I think if it does eventually get deprecated, then we can use whatever the new, recommended interface is, but that will likely wipe out the gains here. One could also do this in C, and get the gains back. Maybe cross that bridge if/when we get to it.

Regardless, I think there is value in this PR here, now as the Builder class puts all of this into one location and documents it, thanks to your suggestion @stvoutsin .

- While reading a TABLEDATA, BINARY or BINARY2 table, keep the values
  and the missing-value flags in plain numpy arrays that can be resized
  in place, and make the masked array once at the end. Before, the whole
  table was copied each time the arrays filled up, and once more at the
  end to trim them.
- Reading the 2.5 GB Gaia table from astropygh-8946 peaks at 5.1 GB instead of
  6.8 GB, and takes 113s instead of 130s.  That's a 25% reduction in
  memory and a 15% speed increase.
- Result is the same: same type, dtype, mask, fill value and values.
- Add a test reading a 1000-row table in each format, which grows the
  arrays past the first 512 rows and then trims them.
- Replace the two helper functions with _MaskedArrayBuilder, which owns
  the data and mask arrays, resizes them together and never hands them
  out, so no view of them can outlive a refcheck=False resize. The
  parsers set rows through set_row and set_rows.
- Test reading a variable-length column longer than the first
  allocation, and tables whose nrows attribute is too small, correct
  or too large, in each format.
@jdavies-st
jdavies-st force-pushed the votable-grow-arrays-in-place branch from daf09d3 to 2b35b7a Compare November 10, 2026 22:06
@pllim

pllim commented Nov 10, 2026

Copy link
Copy Markdown
Member

I see an approval. Providing CI passes, is this good to merge?

@pllim pllim added the Extra CI Run cron CI as part of PR label Nov 10, 2026
@stvoutsin

Copy link
Copy Markdown
Contributor

One general thing I'll add is that numpy is considering deprecating .resize. See

numpy/numpy#28800 (comment)

though there is some pushback for precisely our use case, and that it is also used internally within numpy. I think if it does eventually get deprecated, then we can use whatever the new, recommended interface is, but that will likely wipe out the gains here. One could also do this in C, and get the gains back. Maybe cross that bridge if/when we get to it.

Regardless, I think there is value in this PR here, now as the Builder class puts all of this into one location and documents it, thanks to your suggestion @stvoutsin .

Thanks, that's useful to know!
Agreed on crossing that bridge if we get there and at least now it would only mean changing _MaskedArrayBuilder rather than both parsers. I guess when the file has a correct nrows we don't resize so for those cases we should be fine? Not sure out in the wild how common / uncommon that is though.

In any case, this all looks good to go from my pov!

@pllim

pllim commented Nov 10, 2026

Copy link
Copy Markdown
Member

crossing that bridge if we get there

Do we need a reminder issue or you just want to YOLO this? :)

@jdavies-st

jdavies-st commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor Author

YOLO!

If .resize is eventually deprecated, we will get a very loud deprecation warning from several places in the repo.

Feel free to merge once the tests complete. The current test failures are the usual culprits from the weekly pytest API change and the interwebs being on fire.

@jdavies-st

Copy link
Copy Markdown
Contributor Author

Green! :trollface:

Srsly, even the benchmarks are wacky. Unrelated coordinates and io_ascii performance decreases.

@pllim

pllim commented Nov 11, 2026

Copy link
Copy Markdown
Member

Hmm benchmark report is a bit weird but maybe because your branch is not up-to-date with main? The votable ones look good. The slower ones don't look related.

Change Before [131a74f] After [6020b85] Ratio Benchmark (Parameter)
- 3.16±0s 2.40±0s 0.76 votable.TimeVOTableBitArrayOptimization.time_bitarray_8bit_binary2
- 11.9±0.1ms 8.26±0.05ms 0.7 votable.TimeVOTableSmallOverhead.time_small_binary2
- 1.56±0s 1.03±0.01s 0.66 votable.TimeVOTableLongStrings.time_long_strings_binary
- 6.60±0.3ms 3.63±0.07ms 0.55 votable.TimeVOTableSmallOverhead.time_small_binary
Change Before [131a74f] After [6020b85] Ratio Benchmark (Parameter)
+ 867±30ns 1.31±0.4μs 1.51 io_ascii.core.CoreSuite.time_base_splitter
+ 149±1μs 221±50μs 1.48 coordinates.FrameBenchmarks.time_init_scalar
+ 4.35±0.1ms 5.96±1ms 1.37 coordinates.SkyCoordBenchmarks.time_init_repr_array_noframe
+ 437±8μs 594±20μs 1.36 coordinates.FrameBenchmarks.time_init_scalar_diff

@pllim
pllim merged commit 1ad7032 into astropy:main Nov 11, 2026
53 of 59 checks passed
@pllim

pllim commented Nov 11, 2026

Copy link
Copy Markdown
Member

Thanks, all!

@pllim

pllim commented Nov 11, 2026

Copy link
Copy Markdown
Member

p.s. Wow I didn't know :trollface: exists here.

@jdavies-st
jdavies-st deleted the votable-grow-arrays-in-place branch November 11, 2026 07:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

benchmark Run benchmarks for a PR Extra CI Run cron CI as part of PR io.votable

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants