@@ -12,9 +12,10 @@ for time, and a table that hid the cost would not be worth publishing.
1212
1313## What is measured
1414
15- Fifteen cases on a GitHub-hosted runner: concat (inner, outer, ` --merge same ` ),
16- subset by query, h5ad→zarr, ` view ` , ` ls ` , ` create ` , ` export ` dataframe / array /
17- sparse, ` import dataframe ` , ` split ` , and a peak-RSS-against-input-size sweep. Inputs
15+ Seventeen cases on a GitHub-hosted runner: concat (inner, outer, ` --merge same ` ),
16+ subset by query, h5ad→zarr (` convert ` ), ` view ` , ` ls ` , ` create ` , ` export ` dataframe /
17+ array / sparse, ` import dataframe ` , ` split ` , ` recast ` dtype and layout, and a
18+ peak-RSS-against-input-size sweep. Inputs
1819are 50,000 × 20,000 at 5% density, plus the 2,000 × 36,601 shape that hung in 0.5.1.
1920
2021Every run also reports a startup floor (` --version ` for each contender), because the
@@ -25,7 +26,7 @@ the entire measurement.
2526library offers an equivalent, so the rows would only ever read ` n/a ` . They are covered
2627by the complexity guards instead.
2728
28- ## Results — ` 0.6.1 `
29+ ## Results — ` 0.7.0 `
2930
3031| | |
3132| ---| ---|
@@ -35,28 +36,28 @@ by the complexity guards instead.
3536| Address-space ceiling | 12.9 GB per process |
3637| Repeats | 3 (fastest shown) |
3738| Platform | Linux x86_64, Python 3.12.3 |
38- | Versions | adata-cli 0.6.1 , anndata 0.13.4, scanpy 1.12.4 |
39- | Generated | 2026-09-24T15:41:03Z |
39+ | Versions | adata-cli 0.7.0 , anndata 0.13.4, scanpy 1.12.4 |
40+ | Generated | 2026-10-15T12:48:08Z |
4041
4142### ` startup `
4243
4344What does each contender cost before it does any work?
4445
4546| Contender | Wall time | Peak RSS | Output | vs previous |
4647| ---| ---| ---| ---| ---|
47- | adata-cli | 0.56 s | 69.4 MB | - | no change |
48- | anndata | 1.52 s | 132.8 MB | - | no change |
49- | scanpy | 3.30 s | 265.2 MB | - | no change |
48+ | adata-cli | 0.63 s | 67.9 MB | - | time +13% |
49+ | anndata | 1.65 s | 133.5 MB | - | no change |
50+ | scanpy | 3.57 s | 260.3 MB | - | no change |
5051
5152### ` concat-inner `
5253
5354Concatenate two stores on the obs axis, inner join.
5455
5556| Contender | Wall time | Peak RSS | Output | vs previous |
5657| ---| ---| ---| ---| ---|
57- | adata-cli | 12.26 s | 177.2 MB | 583.3 MB | time +108 % |
58- | anndata (concat_on_disk) | 6.18 s | 451.0 MB | 831.9 MB | no change |
59- | anndata (in memory) | 12.26 s | 1.8 GB | 580.3 MB | no change |
58+ | adata-cli | 9.29 s | 170.3 MB | 583.3 MB | time -24 % |
59+ | anndata (concat_on_disk) | 5.94 s | 447.3 MB | 831.9 MB | no change |
60+ | anndata (in memory) | 9.09 s | 1.8 GB | 580.3 MB | time -26% |
6061| scanpy | n/a | n/a | n/a | - |
6162| | * scanpy has no concat of its own; it re-exports anndata's* | | | |
6263
@@ -66,29 +67,29 @@ Concatenate carrying var columns forward -- the REQ-71798 case.
6667
6768| Contender | Wall time | Peak RSS | Output | vs previous |
6869| ---| ---| ---| ---| ---|
69- | adata-cli | 1.74 s | 195.6 MB | 45.7 MB | time +40 % |
70- | anndata (concat_on_disk) | 2.26 s | 247.6 MB | 63.2 MB | no change |
71- | anndata (in memory) | 2.67 s | 288.0 MB | 44.6 MB | no change |
70+ | adata-cli | 1.50 s | 193.1 MB | 45.7 MB | time -14 % |
71+ | anndata (concat_on_disk) | 2.24 s | 246.3 MB | 63.2 MB | no change |
72+ | anndata (in memory) | 2.42 s | 288.9 MB | 44.6 MB | no change |
7273
7374### ` subset-query `
7475
7576Keep the obs rows matching a predicate on an existing column.
7677
7778| Contender | Wall time | Peak RSS | Output | vs previous |
7879| ---| ---| ---| ---| ---|
79- | adata-cli | 4.10 s | 212.0 MB | 147.1 MB | time +26%, RSS -39 % |
80- | anndata (backed) | 4.94 s | 626.5 MB | 146.6 MB | no change |
81- | anndata (in memory) | 5.21 s | 984.4 MB | 146.6 MB | no change |
82- | scanpy (filter_cells) | 9.02 s | 1.6 GB | 291.2 MB | no change |
80+ | adata-cli | 3.35 s | 209.5 MB | 147.1 MB | time -18 % |
81+ | anndata (backed) | 4.26 s | 683.4 MB | 146.6 MB | time -14% |
82+ | anndata (in memory) | 4.49 s | 985.2 MB | 146.6 MB | time -14% |
83+ | scanpy (filter_cells) | 7.69 s | 1.6 GB | 291.2 MB | time -15% |
8384
8485### ` h5ad-to-zarr `
8586
8687Convert a store to Zarr.
8788
8889| Contender | Wall time | Peak RSS | Output | vs previous |
8990| ---| ---| ---| ---| ---|
90- | adata-cli | 7.81 s | 232.9 MB | 151.6 MB in 4717 files | time +29 % |
91- | anndata (in memory) | 6.15 s | 814.9 MB | 136.8 MB in 55 files | no change |
91+ | adata-cli | 6.14 s | 154.2 MB | 151.6 MB in 4717 files | time -21%, RSS -34 % |
92+ | anndata (in memory) | 5.70 s | 778.4 MB | 136.8 MB in 55 files | no change |
9293| scanpy | n/a | n/a | n/a | - |
9394| | * no streaming converter; scanpy defers to anndata* | | | |
9495
@@ -98,37 +99,37 @@ Report what is in the store, without reading the matrix.
9899
99100| Contender | Wall time | Peak RSS | Output | vs previous |
100101| ---| ---| ---| ---| ---|
101- | adata-cli | 0.58 s | 71.4 MB | - | no change |
102- | anndata (read_elem on obs) | 1.76 s | 159.9 MB | - | no change |
103- | anndata (full load) | 3.20 s | 583.7 MB | - | no change |
102+ | adata-cli | 0.61 s | 69.8 MB | - | no change |
103+ | anndata (read_elem on obs) | 1.76 s | 160.3 MB | - | no change |
104+ | anndata (full load) | 3.19 s | 584.3 MB | - | no change |
104105
105106### ` export-obs `
106107
107108Write the obs table out as CSV.
108109
109110| Contender | Wall time | Peak RSS | Output | vs previous |
110111| ---| ---| ---| ---| ---|
111- | adata-cli | 0.74 s | 85.1 MB | 1.8 MB | no change |
112- | anndata (read_elem on obs) | 1.77 s | 160.5 MB | 1.8 MB | no change |
113- | anndata (full load) | 3.28 s | 583.9 MB | 1.8 MB | no change |
112+ | adata-cli | 0.77 s | 83.3 MB | 1.8 MB | no change |
113+ | anndata (read_elem on obs) | 1.78 s | 161.1 MB | 1.8 MB | no change |
114+ | anndata (full load) | 3.22 s | 584.4 MB | 1.8 MB | no change |
114115
115116### ` split-by-sample `
116117
117118Write one store per distinct value of an obs column.
118119
119120| Contender | Wall time | Peak RSS | Output | vs previous |
120121| ---| ---| ---| ---| ---|
121- | adata-cli | 6.65 s | 198.4 MB | 302.6 MB in 9 files | time -88%, RSS -74 % |
122- | anndata (hand-written loop) | 7.81 s | 684.9 MB | 301.7 MB in 8 files | no change |
122+ | adata-cli | 4.99 s | 196.5 MB | 302.6 MB in 9 files | time -25 % |
123+ | anndata (hand-written loop) | 6.34 s | 684.4 MB | 301.7 MB in 8 files | time -19% |
123124
124125### ` ls `
125126
126127List everything in the store.
127128
128129| Contender | Wall time | Peak RSS | Output | vs previous |
129130| ---| ---| ---| ---| ---|
130- | adata-cli | 0.59 s | 70.5 MB | - | no change |
131- | h5py (visit) | 1.66 s | 136.3 MB | - | no change |
131+ | adata-cli | 0.63 s | 68.9 MB | - | no change |
132+ | h5py (visit) | 1.67 s | 136.4 MB | - | no change |
132133| h5ls -r | n/a | n/a | n/a | - |
133134| | * h5ls is not installed* | | | |
134135
@@ -138,56 +139,56 @@ Write an empty store with a given obs/var shape.
138139
139140| Contender | Wall time | Peak RSS | Output | vs previous |
140141| ---| ---| ---| ---| ---|
141- | adata-cli | 0.64 s | 84 .5 MB | 3.4 MB | no change |
142- | anndata | 8.73 s | 228 .1 MB | 49.2 MB | no change |
142+ | adata-cli | 0.66 s | 82 .5 MB | 3.4 MB | no change |
143+ | anndata | 9.00 s | 229 .1 MB | 49.2 MB | no change |
143144
144145### ` export-array `
145146
146147Write a dense element out as .npy.
147148
148149| Contender | Wall time | Peak RSS | Output | vs previous |
149150| ---| ---| ---| ---| ---|
150- | adata-cli | 0.60 s | 82.6 MB | 10.0 MB | no change |
151- | anndata (read_elem) | 1.67 s | 154.3 MB | 10.0 MB | no change |
152- | anndata (full load) | 3.27 s | 583.8 MB | 10.0 MB | no change |
151+ | adata-cli | 0.66 s | 81.3 MB | 10.0 MB | time +10% |
152+ | anndata (read_elem) | 1.68 s | 154.6 MB | 10.0 MB | no change |
153+ | anndata (full load) | 3.22 s | 584.3 MB | 10.0 MB | no change |
153154
154155### ` export-sparse `
155156
156157Write X out as Matrix Market text.
157158
158159| Contender | Wall time | Peak RSS | Output | vs previous |
159160| ---| ---| ---| ---| ---|
160- | adata-cli | 25.52 s | 76.3 MB | 761.3 MB | no change |
161- | anndata (sparse_dataset) | 7.12 s | 968.7 MB | 661.6 MB | no change |
162- | anndata (full load) | 7.23 s | 775.1 MB | 661.6 MB | no change |
161+ | adata-cli | 21.22 s | 74.7 MB | 761.3 MB | time -17% |
162+ | anndata (sparse_dataset) | 6.53 s | 968.5 MB | 661.6 MB | no change |
163+ | anndata (full load) | 6.72 s | 774.4 MB | 661.6 MB | no change |
163164
164165### ` import-dataframe `
165166
166167Replace obs from a CSV.
167168
168169| Contender | Wall time | Peak RSS | Output | vs previous |
169170| ---| ---| ---| ---| ---|
170- | adata-cli | 1.21 s | 114.2 MB | 296.0 MB | no change |
171- | anndata | 6.88 s | 596 .5 MB | 291.2 MB | no change |
171+ | adata-cli | 1.96 s | 112.4 MB | 296.0 MB | time +63% |
172+ | anndata | 5.36 s | 600 .5 MB | 291.2 MB | time -22% |
172173
173174### ` concat-outer `
174175
175176Concatenate two stores keeping the union of variables.
176177
177178| Contender | Wall time | Peak RSS | Output | vs previous |
178179| ---| ---| ---| ---| ---|
179- | adata-cli | 12.20 s | 172.0 MB | 583.3 MB | time +106 % |
180- | anndata (concat_on_disk) | 6.26 s | 450.5 MB | 831.9 MB | no change |
181- | anndata (in memory) | 12.18 s | 1.8 GB | 580.3 MB | no change |
180+ | adata-cli | 9.15 s | 175.6 MB | 583.3 MB | time -25 % |
181+ | anndata (concat_on_disk) | 5.93 s | 449.6 MB | 831.9 MB | no change |
182+ | anndata (in memory) | 9.07 s | 1.8 GB | 580.3 MB | time -26% |
182183
183184### ` convert-dtype `
184185
185186Rewrite X as float32.
186187
187188| Contender | Wall time | Peak RSS | Output | vs previous |
188189| ---| ---| ---| ---| ---|
189- | adata-cli | 5.57 s | 90 .2 MB | 291.1 MB | no change |
190- | anndata (in memory) | 6.91 s | 983.8 MB | 291.2 MB | no change |
190+ | adata-cli | 4.02 s | 88 .2 MB | 291.1 MB | time -28% |
191+ | anndata (in memory) | 5.35 s | 984.0 MB | 291.2 MB | time -23% |
191192| scanpy | n/a | n/a | n/a | - |
192193| | * no dtype rewrite; scanpy defers to anndata* | | | |
193194
@@ -197,18 +198,18 @@ Transpose X from CSR to CSC.
197198
198199| Contender | Wall time | Peak RSS | Output | vs previous |
199200| ---| ---| ---| ---| ---|
200- | adata-cli (streaming) | 13.62 s | 369.8 MB | 1.2 GB | no change |
201- | adata-cli (--in-memory) | 9.52 s | 2.3 GB | 297.0 MB | no change |
202- | anndata (in memory) | 7.22 s | 984.2 MB | 297.0 MB | no change |
201+ | adata-cli (streaming) | 18.18 s | 362.2 MB | 297.0 MB | time +33% |
202+ | adata-cli (--in-memory) | 8.85 s | 2.3 GB | 297.0 MB | no change |
203+ | anndata (in memory) | 6.04 s | 984.7 MB | 297.0 MB | time -16% |
203204
204205### ` rss-vs-size `
205206
206207Does peak memory track input size, at a fixed --chunk? This is the README's claim, stated as a measurement.
207208
208209| Contender | Wall time | Peak RSS | Output | vs previous |
209210| ---| ---| ---| ---| ---|
210- | adata-cli | 12.13 s | 173.8 MB | 583.3 MB | time +105 % |
211- | anndata (in memory) | 12.18 s | 1.8 GB | 580.3 MB | no change |
211+ | adata-cli | 9.19 s | 180.6 MB | 583.3 MB | time -24 % |
212+ | anndata (in memory) | 8.99 s | 1.8 GB | 580.3 MB | time -26% |
212213
213214---
214215
@@ -222,6 +223,7 @@ Rows marked `n/a` are operations the baseline does not offer; that is a result,
222223
223224| Run | Ref | Tier | Wall time | Peak RSS | Raw |
224225| ---| ---| ---| ---| ---| ---|
226+ | 2026-10-15T12:48:08Z | ` 0.7.0 ` | ci | 9.29 s | 170.3 MB | [ json] ( benchmarks/0.7.0.json ) |
225227| 2026-09-24T15:41:03Z | ` 0.6.1 ` | ci | 12.26 s | 177.2 MB | [ json] ( benchmarks/0.6.1.json ) |
226228| 2026-09-24T10:35:13Z | ` 0.6.0 ` | ci | 5.90 s | 176.2 MB | [ json] ( benchmarks/0.6.0.json ) |
227229
0 commit comments