Skip to content

ASV better memory benchmarks - #1609

Open
cmdupuis3 wants to merge 5 commits into
UXARRAY:mainfrom
cmdupuis3:cmd/asv-peakmem-benchmark-fix
Open

ASV better memory benchmarks#1609
cmdupuis3 wants to merge 5 commits into
UXARRAY:mainfrom
cmdupuis3:cmd/asv-peakmem-benchmark-fix

Conversation

@cmdupuis3

@cmdupuis3 cmdupuis3 commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

Closes #1605

Overview

The current behavior of a few peakmem benchmarks entrains module imports and caching, which dwarf the actual memory usage supposedly being tested. This PR offers benchmarks of low-level memory behavior as well as warm setups to prevent the persistent "every PR improves memory by the same amount" issue in #1605.

Expected Usage

Most of these benchmarks are useful as-is, but there are also a couple of benchmarks of pure uxarray imports to compare with if the illusory memory improvement ever returns.

PR Checklist

General

  • An issue is linked created and linked
  • Add appropriate labels
  • Filled out Overview and Expected Usage (if applicable) sections

Testing

  • Adequate tests are created if there is new functionality
  • Tests cover all possible logical paths in your function
  • Tests are not too basic (such as simply calling a function and nothing else)

Documentation

  • Docstrings have been added to all new functions
  • Docstrings have updated with any function changes

cmdupuis3 and others added 3 commits July 22, 2026 12:52
Three benchmarks reported a near-identical "improvement" on every PR regardless
of what the PR touched:

  578M -> 391M  0.68  face_bounds.FaceBounds.peakmem_face_bounds(geoflow-small)
  708M -> 390M  0.55  face_bounds.FaceBounds.peakmem_face_bounds(quad-hexagon)
  498M -> 384M  0.77  mpas_ocean.Gradient.peakmem_gradient('480km')

They were not measuring the operation under test. asv's peakmem_* records the
max RSS of the whole process, and per asv's docs it "also counts memory usage
during the setup routine". Profiling the face_bounds params:

  grid                    import  +open_grid  +.bounds   attributable to op
  quad-hexagon  ( 24K)     236MB      265MB     301MB     36MB (12%)
  geoflow-small (1.1M)     236MB      263MB     487MB    224MB (46%)
  outCSne8      ( 48K)     235MB      281MB     317MB     35MB (11%)
  oQU480        (4.6M)     236MB      270MB     306MB     36MB (12%)

`import uxarray` alone is ~226 MB and constant to within 1 MB. oQU480 is 190x
larger than quad-hexagon yet both attributed ~36 MB to Grid.bounds -- the
benchmark was nearly insensitive to its own workload. The part that did vary was
numba: uxarray has 81 @njit(cache=True) kernels and both affected paths go
through them (uxarray/grid/bounds.py, uxarray/core/gradient.py). With a cold JIT
cache the quad-hexagon case peaked at 599 MB, with a warm one 298 MB -- a ratio
of 0.50, matching the ratios seen in CI. Which side of an `asv continuous`
comparison paid the compile cost depended on run order, not on the code under
review.

peakmem_* could not be repaired in place, because there was nothing to measure.
Across every operation the five peakmem benchmarks covered, against every mesh
in the suite including the 98 MB / 28,571-face oQU120:

  Grid.bounds        ~1 MB     open_grid   0-10 MB (lazy; mostly allocator noise)
  gradient        0-0.1 MB     integrate       0.0 MB
  open_dataset      0.0 MB

Two better instruments were evaluated and rejected:

  - Sampled peak RSS. Validates cleanly (a known 200 MB allocation measures as
    200.0 MB) and is immune to process history. But the warm-up call needed to
    keep JIT out of the measurement leaves freed pages in the allocator, so RSS
    *growth* undercounts -- every operation measured 0.0-0.1 MB this way.
  - tracemalloc. Measures allocation volume rather than RSS growth, so it would
    sidestep page reuse, but it does not observe numba NRT allocations, which is
    where uxarray's array memory is allocated.

What remains is deterministic size accounting via track_* benchmarks, which also
sidesteps the setup confound entirely -- track_* does not count setup. Verified
byte-identical across cold JIT, warm JIT, and an independent second cold JIT for
all 14 parameter combinations, and it scales correctly with the mesh
(quad-hexagon reports 128 bytes = 4 faces x 32). It does not capture transient
peaks, but the measurements above show those are ~1 MB, far below anything worth
gating on, and no available instrument captures them reliably here.

The rationale is recorded in benchmarks/_memsize.py so the next person does not
reintroduce peakmem_*. The commented-out mem_* stubs in quad_hexagon.py, an
earlier attempt at the same thing, are removed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The previous commit removed peakmem_* because the per-operation memory it
claimed to measure was ~1 MB against a reported 400-700 MB. But the larger
question those benchmarks were reaching for -- "how much memory does it take to
open this grid and compute on it, from a cold start" -- is real, and asv can
answer it with its own peakmem_* once two things are controlled:

  1. Process history. ru_maxrss is a high-water mark that never falls, so
     imports, setup() and earlier work in the same process set a floor under the
     result. asv already spawns a separate process per benchmark *and per
     parameter* (runner._run_benchmark_single_param), so this is handled as long
     as the benchmark classes declare no setup() -- asv counts setup memory
     towards peakmem_*, which is exactly the confound its own docs warn about.

  2. numba JIT cache warmth. Compiling uxarray's 81 @njit(cache=True) kernels
     costs a few hundred MB of transient RSS, so an otherwise identical run
     peaked at 599 MB cold and 298 MB warm. Whichever side of an `asv continuous`
     comparison happened to compile paid that cost.

setup_cache handles (2). asv runs it in its own process
(runner.Spawner.create_setup_cache), so LLVM's footprint never enters the
high-water mark of the processes that do the measuring; they load the compiled
kernels from numba's on-disk cache instead. It runs once per commit, so both
sides of a comparison are warmed symmetrically. Every parameter is warmed, not
just one -- the grids do not all reach the same njit signatures.

Measured via `asv run --python=same --quick -b peakmem`, twice warm and once
after deleting every .nbi/.nbc in the installed uxarray:

                              warm    warm    COLD    spread
  import uxarray              280M    282M    280M      0.7%
  open+bounds quad-hexagon    349M    349M    346M      0.9%
  open+bounds geoflow-small   348M    348M    348M      0.0%
  open+bounds outCSne8        365M    364M    370M      1.6%
  open+bounds oQU480          354M    356M    351M      1.4%
  gradient 480km              358M    355M    354M      1.1%
  gradient 120km              371M    370M    381M      2.9%

The cold column is the condition that used to halve the result; the cache
repopulated from 0 to 33 entries during that run, confirming setup_cache did the
compiling. Everything sits inside 2.9%, against asv's default 10% factor.

peakmem_import_uxarray is included deliberately. ~280 MB of every row above is
just importing uxarray, so tracking it on its own means a heavy new top-level
import shows up as itself instead of silently inflating everything else.

Also moves _memsize into benchmarks/helpers/ and gives it an __init__.py, so the
package structure is explicit rather than relying on namespace packages.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@cmdupuis3 cmdupuis3 added bug Something isn't working benchmarking Related to benchmarks, memory usage, and/or time profiling run-benchmark Run ASV benchmark workflow labels Jul 22, 2026
@github-actions

github-actions Bot commented Jul 22, 2026

Copy link
Copy Markdown

ASV Benchmarking

Benchmark Comparison Results

Benchmarks that have stayed the same:

Change Before [852a0c0] After [f69d4e6] Ratio Benchmark (Parameter)
10.8±0.2μs 10.3±0.1μs 0.95 bench_connectivity.Connectivity.time_edge_face('120km')
10.8±0.2μs 10.7±0.3μs 0.99 bench_connectivity.Connectivity.time_edge_face('480km')
10.5±0.2μs 10.5±0.1μs 1.01 bench_connectivity.Connectivity.time_edge_node('120km')
11.0±0.2μs 11.1±0.06μs 1.00 bench_connectivity.Connectivity.time_edge_node('480km')
10.3±0.1μs 10.5±0.08μs 1.02 bench_connectivity.Connectivity.time_face_edge('120km')
11.1±0.1μs 11.0±0.2μs 0.98 bench_connectivity.Connectivity.time_face_edge('480km')
10.4±0.1μs 10.4±0.1μs 1.00 bench_connectivity.Connectivity.time_face_face('120km')
11.1±0.2μs 10.9±0.1μs 0.98 bench_connectivity.Connectivity.time_face_face('480km')
20.8±0.2μs 21.1±0.1μs 1.02 bench_connectivity.Connectivity.time_face_node('120km')
22.0±0.3μs 21.4±0.3μs 0.97 bench_connectivity.Connectivity.time_face_node('480km')
10.5±0.2μs 10.2±0.1μs 0.97 bench_connectivity.Connectivity.time_node_edge('120km')
10.9±0.1μs 11.0±0.2μs 1.01 bench_connectivity.Connectivity.time_node_edge('480km')
10.7±0.1μs 10.7±0.1μs 1.00 bench_connectivity.Connectivity.time_node_face('120km')
10.9±0.2μs 10.8±0.1μs 0.98 bench_connectivity.Connectivity.time_node_face('480km')
24.6±0.4μs 23.4±0.3μs 0.95 face_bounds.FaceBounds.time_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/mpas/QU/oQU480.231010.nc'))
10.5±0.07μs 10.5±0.08μs 1.00 face_bounds.FaceBounds.time_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/scrip/outCSne8/outCSne8.nc'))
11.6±0.3ms 10.4±0.2ms ~0.90 face_bounds.FaceBounds.time_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/geoflow-small/grid.nc'))
2.09±0.01ms 2.13±0.05ms 1.02 face_bounds.FaceBounds.time_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/quad-hexagon/grid.nc'))
57.3k 57.3k 1.00 face_bounds.FaceBounds.track_nbytes_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/mpas/QU/oQU480.231010.nc'))
12.3k 12.3k 1.00 face_bounds.FaceBounds.track_nbytes_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/scrip/outCSne8/outCSne8.nc'))
123k 123k 1.00 face_bounds.FaceBounds.track_nbytes_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/geoflow-small/grid.nc'))
128 128 1.00 face_bounds.FaceBounds.track_nbytes_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/quad-hexagon/grid.nc'))
1.27M 1.27M 1.00 face_bounds.FaceBounds.track_nbytes_grid_with_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/mpas/QU/oQU480.231010.nc'))
50.1k 50.1k 1.00 face_bounds.FaceBounds.track_nbytes_grid_with_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/scrip/outCSne8/outCSne8.nc'))
1.48M 1.48M 1.00 face_bounds.FaceBounds.track_nbytes_grid_with_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/geoflow-small/grid.nc'))
712 712 1.00 face_bounds.FaceBounds.track_nbytes_grid_with_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/quad-hexagon/grid.nc'))
335M 334M 1.00 face_bounds.FaceBoundsPeakMem.peakmem_open_and_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/mpas/QU/oQU480.231010.nc'))
365M 366M 1.00 face_bounds.FaceBoundsPeakMem.peakmem_open_and_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/scrip/outCSne8/outCSne8.nc'))
336M 336M 1.00 face_bounds.FaceBoundsPeakMem.peakmem_open_and_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/geoflow-small/grid.nc'))
335M 335M 1.00 face_bounds.FaceBoundsPeakMem.peakmem_open_and_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/quad-hexagon/grid.nc'))
937±9ns 943±2ns 1.01 geometry_kernels.AccucrossKernels.time_accucross
2.48±0μs 2.46±0μs 0.99 geometry_kernels.AccucrossKernels.time_accucross_pair
292±0.7ns 293±1ns 1.00 geometry_kernels.EFTPrimitives.time_acc_sqrt_re
279±1ns 282±2ns 1.01 geometry_kernels.EFTPrimitives.time_diff_of_products
232±0.4ns 234±0.3ns 1.01 geometry_kernels.EFTPrimitives.time_two_prod
236±0.7ns 236±0.4ns 1.00 geometry_kernels.EFTPrimitives.time_two_sum
1.20±0μs 1.21±0.03μs 1.01 geometry_kernels.GCAConstLatIntersection.time_accux_constlat_kernel
858±3ns 862±4ns 1.00 geometry_kernels.GCAConstLatIntersection.time_gca_const_lat_intersection
1.61±0.02μs 1.62±0.03μs 1.01 geometry_kernels.GCAConstLatIntersection.time_try_gca_const_lat_intersection
1.32±0μs 1.33±0.01μs 1.01 geometry_kernels.GCAGCAIntersection.time_accux_gca_kernel
1.12±0.01μs 1.12±0μs 1.00 geometry_kernels.GCAGCAIntersection.time_gca_gca_intersection
1.83±0.01μs 1.84±0.01μs 1.00 geometry_kernels.GCAGCAIntersection.time_try_gca_gca_intersection
51.6±0.2μs 49.3±0.4μs 0.96 geometry_kernels.OrientPredicates.time_on_minor_arc
508±2ns 546±30ns 1.08 geometry_kernels.OrientPredicates.time_orient3d_on_sphere
2.72±0.1ms 2.61±0.01ms 0.96 geometry_samebody.SameBodyConstLat.time_accux_dispatch
1.17±0.01ms 1.17±0ms 1.00 geometry_samebody.SameBodyConstLat.time_accux_kernel
1.72±0.01ms 1.73±0ms 1.00 geometry_samebody.SameBodyConstLat.time_fp64_dispatch
146±0.3μs 146±0.1μs 1.00 geometry_samebody.SameBodyConstLat.time_fp64_kernel
32.5±0.1ms 32.3±0.03ms 0.99 geometry_samebody_gcagca.SameBodyGcaGca.time_accux_dispatch
10.2±0.05ms 10.2±0.02ms 1.00 geometry_samebody_gcagca.SameBodyGcaGca.time_accux_kernel
26.5±0.05ms 27.1±0.6ms 1.02 geometry_samebody_gcagca.SameBodyGcaGca.time_fp64_dispatch
4.89±0.02ms 4.89±0.02ms 1.00 geometry_samebody_gcagca.SameBodyGcaGca.time_fp64_kernel
292M 293M 1.00 import.Imports.peakmem_import_uxarray
893±20ms 864±40ms 0.97 import.Imports.timeraw_import_uxarray
923±10ns 947±20ns 1.03 mpas_ocean.CheckNorm.time_check_norm('120km')
895±10ns 896±10ns 1.00 mpas_ocean.CheckNorm.time_check_norm('480km')
862±9ms 854±10ms 0.99 mpas_ocean.ConnectivityConstruction.time_face_face_connectivity('120km')
55.1±0.3ms 55.3±0.7ms 1.00 mpas_ocean.ConnectivityConstruction.time_face_face_connectivity('480km')
14.6±0.09μs 14.4±0.06μs 0.99 mpas_ocean.ConnectivityConstruction.time_n_nodes_per_face('120km')
14.5±0.09μs 14.9±0.06μs 1.03 mpas_ocean.ConnectivityConstruction.time_n_nodes_per_face('480km')
5.00±0.04ms 4.88±0.06ms 0.97 mpas_ocean.ConstructFaceLatLon.time_cartesian_averaging('120km')
3.48±0.04ms 3.39±0.03ms 0.97 mpas_ocean.ConstructFaceLatLon.time_cartesian_averaging('480km')
3.47±0.01s 3.55±0.03s 1.02 mpas_ocean.ConstructFaceLatLon.time_welzl('120km')
225±1ms 223±0.8ms 0.99 mpas_ocean.ConstructFaceLatLon.time_welzl('480km')
18.3±0.02ms 18.3±0.02ms 1.00 mpas_ocean.ConstructTreeStructures.time_ball_tree('120km')
1.12±0.01ms 1.12±0.05ms 1.00 mpas_ocean.ConstructTreeStructures.time_ball_tree('480km')
10.7±0.02ms 10.7±0.02ms 0.99 mpas_ocean.ConstructTreeStructures.time_kd_tree('120km')
777±10μs 787±30μs 1.01 mpas_ocean.ConstructTreeStructures.time_kd_tree('480km')
756±5ms 768±20ms 1.02 mpas_ocean.CrossSections.time_const_lat('120km', 1)
379±2ms 374±3ms 0.99 mpas_ocean.CrossSections.time_const_lat('120km', 2)
193±2ms 196±4ms 1.02 mpas_ocean.CrossSections.time_const_lat('120km', 4)
587±5ms 562±6ms 0.96 mpas_ocean.CrossSections.time_const_lat('480km', 1)
290±2ms 286±5ms 0.99 mpas_ocean.CrossSections.time_const_lat('480km', 2)
149±1ms 149±3ms 1.00 mpas_ocean.CrossSections.time_const_lat('480km', 4)
25.7±0.1ms 25.5±0.3ms 0.99 mpas_ocean.DualMesh.time_dual_mesh_construction('120km')
3.80±0.09ms 3.51±0.4ms 0.93 mpas_ocean.DualMesh.time_dual_mesh_construction('480km')
978±2ms 950±9ms 0.97 mpas_ocean.GeoDataFrame.time_to_geodataframe('120km', False)
52.2±0.4ms 55.0±0.7ms 1.05 mpas_ocean.GeoDataFrame.time_to_geodataframe('120km', True)
85.5±0.3ms 84.7±1ms 0.99 mpas_ocean.GeoDataFrame.time_to_geodataframe('480km', False)
6.07±0.6ms 5.94±0.07ms 0.98 mpas_ocean.GeoDataFrame.time_to_geodataframe('480km', True)
179±2ms 177±2ms 0.99 mpas_ocean.Gradient.time_gradient('120km')
13.4±0.1ms 12.7±0.3ms 0.95 mpas_ocean.Gradient.time_gradient('480km')
457k 457k 1.00 mpas_ocean.Gradient.track_nbytes_gradient('120km')
28.7k 28.7k 1.00 mpas_ocean.Gradient.track_nbytes_gradient('480km')
350M 350M 1.00 mpas_ocean.GradientPeakMem.peakmem_gradient('120km')
330M 330M 1.00 mpas_ocean.GradientPeakMem.peakmem_gradient('480km')
226±2μs 229±3μs 1.01 mpas_ocean.HoleEdgeIndices.time_construct_hole_edge_indices('120km')
135±1μs 135±2μs 1.00 mpas_ocean.HoleEdgeIndices.time_construct_hole_edge_indices('480km')
218±0.7μs 221±2μs 1.01 mpas_ocean.Integrate.time_integrate('120km')
202±2μs 204±1μs 1.01 mpas_ocean.Integrate.time_integrate('480km')
18.4M 18.4M 1.00 mpas_ocean.Integrate.track_nbytes_integrate('120km')
1.2M 1.2M 1.00 mpas_ocean.Integrate.track_nbytes_integrate('480km')
186±2ms 186±2ms 1.00 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('120km', 'exclude')
186±2ms 189±2ms 1.02 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('120km', 'include')
186±2ms 189±1ms 1.02 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('120km', 'split')
14.6±0.3ms 15.4±0.5ms 1.05 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('480km', 'exclude')
14.5±0.4ms 15.1±0.03ms 1.04 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('480km', 'include')
14.2±0.1ms 14.2±0.7ms 1.00 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('480km', 'split')
362±3μs 362±2μs 1.00 mpas_ocean.PointInPolygon.time_face_search_lonlat('120km')
368±3μs 372±3μs 1.01 mpas_ocean.PointInPolygon.time_face_search_lonlat('480km')
340±2μs 344±4μs 1.01 mpas_ocean.PointInPolygon.time_face_search_xyz('120km')
340±2μs 342±2μs 1.01 mpas_ocean.PointInPolygon.time_face_search_xyz('480km')
250±1ms 247±1ms 0.99 mpas_ocean.RemapDownsample.time_bilinear_remapping
295±1ms 297±6ms 1.00 mpas_ocean.RemapDownsample.time_inverse_distance_weighted_remapping
5.03±0.3ms 5.36±0.1ms 1.07 mpas_ocean.RemapDownsample.time_nearest_neighbor_remapping
1.48±0.03s 1.48±0.03s 1.00 mpas_ocean.RemapUpsample.time_bilinear_remapping
39.1±0.9ms 40.3±0.8ms 1.03 mpas_ocean.RemapUpsample.time_inverse_distance_weighted_remapping
10.1±0.2ms 10.4±0.2ms 1.03 mpas_ocean.RemapUpsample.time_nearest_neighbor_remapping
6.58±0.5ms 6.93±0.1ms 1.05 mpas_ocean.ZonalAverage.time_zonal_average('480km')
8.30±0.4ms 8.91±0.5ms 1.07 quad_hexagon.QuadHexagon.time_open_dataset
7.53±0.7ms 7.72±0.2ms 1.02 quad_hexagon.QuadHexagon.time_open_grid
408 408 1.00 quad_hexagon.QuadHexagon.track_nbytes_open_dataset
392 392 1.00 quad_hexagon.QuadHexagon.track_nbytes_open_grid

Benchmarks that have got worse:

Change Before [852a0c0] After [f69d4e6] Ratio Benchmark (Parameter)
+ 26.8±0.9ms 29.8±0.3ms 1.11 mpas_ocean.ZonalAverage.time_zonal_average('120km')

@Sevans711

Copy link
Copy Markdown
Collaborator

Thank you for working on this and opening up this PR! I have a few quick thoughts before reviewing:

  1. I wonder if nbytes should be added directly as a property of Grid objects, instead of needing a custom grid_nbytes function? It might be nice to be able to do uxgrid.nbytes, since Grid is basically just an xr.Dataset with some extra functionality attached to it, and it seems nice to be able to quickly check how large of a grid you are working with.
  2. What is the purpose of benchmarks that measure the size of the loaded Grid objects or arrays? My initial understanding is that the answer should always be the same, and it isn't really measuring runtime efficiency nor total memory usage. Would these work better as unit tests? E.g. "test_face_bounds_array_size" which asserts the size of face_bounds from oQU480.231010.nc is 57.3 kB, with some small tolerance for rounding errors. I might also be not fully understanding the purpose of benchmarks. Also wondering if @erogluorhan or @rajeeja have thoughts on this question in particular?

@cmdupuis3 cmdupuis3 self-assigned this Jul 24, 2026
@cmdupuis3

Copy link
Copy Markdown
Collaborator Author

@Sevans711 as per convo, 1 is really a question for @erogluorhan or @rajeeja. On 2, these are included because the peakmem is not necessarily the same as the total memory usage in a chunked situation. This particular batch of benchmarks is a little verbose, but it basically serves as a regression test against the kinds of spurious improvements in other PRs, so that we have a better idea of which things are bloating the peakmem metrics.

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

Labels

benchmarking Related to benchmarks, memory usage, and/or time profiling bug Something isn't working run-benchmark Run ASV benchmark workflow

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Some ASV peakmem benchmarks always show the same "improvement"

3 participants