Profiling¶
Run time and memory of Baysor cpp-0.9.0, and how they grow with the number of molecules, threads and genes. For what the segmentations look like, see Benchmarks.
All numbers were measured on an old server CPU (Intel Xeon E5-2670, Sandy Bridge generation, 2011–2012); current hardware is expected to be noticeably faster.
Measurement setup
| CPU | Intel Xeon E5-2670 @ 2.60 GHz (Sandy Bridge, 2012), 1 socket, 8 cores / 16 threads, AVX but no AVX2 |
| memory, OS | 60 GB RAM, Ubuntu 26.04 |
| build | from source with the conda GCC 15.3 toolchain: ./configure.sh --deps=conda, CMake Release (-O3 -DNDEBUG, generic x86-64, no -march=native) |
| benchmark runs | cpp-0.9.0 release candidate (perf-optimization @ 1636267, output-identical to the release), 2026-10-01; baysor-benchmarks harness, the dataset's command line plus --skip-ncv-color --output-style parquet; threads set with OMP_NUM_THREADS; wall time and peak RSS from /usr/bin/time -v; 3 replicates at 6 threads (1 for the 2M-molecule crops), 1–2 at 1 thread |
| profiling runs | perf-optimization @ 20bc45c (2026-10-01), which has all the performance work of cpp-0.9.0 (the later commits fix bugs and simplify code), compared with e45fddc (2026-09-30, the start of the optimisation work); cmake --preset profiling = Release plus -g, the same machine code; NCV colours on (the default); CPU time and peak RSS from gperftools and /proc sampling |
The host was shared with other jobs. The 1-minute load average was 5–21 during the benchmark runs and 5–53 during the real-size profiling runs, so wall times include contention; CPU times and memory are less affected. The thread sweep ran one process at a time at a load of 2–13.
Run time and memory on real data¶
Figure 1. Wall time (top) and peak resident memory (bottom) at 6 threads against the number of molecules, for the 26 real-data crops of the benchmark (log–log; median of the replicates). Run time grows roughly linearly with the number of molecules. The labelled crops with thousands of genes sit above the trend. Hover a point for its dataset and run; "Data table" lists every point.
Scaling with the size of the data¶
Figure 2. Real-size ladders: random subsets of a Xenium lung slide and a
Xenium Prime 5K slide, up to the whole slide, and a CosMx
whole-transcriptome slide. CPU time at 8 threads (dotted: linear), wall time
at 8 threads (solid) and 1 thread (dotted; 1-thread runs stop at 2M
molecules), and peak memory at 8 threads. Dashed open marks are the code
before the optimisation (e45fddc). Wall times are load-sensitive: the 8M
lung rung ran at a load of 20, which explains its jump.
Run time and memory per dataset¶
| dataset | molecules | genes | threads | wall time | CPU time | peak RSS | RSS per molecule | wall time, 1 thread | CPU time before | peak RSS before |
|---|---|---|---|---|---|---|---|---|---|---|
| ISS hippocampus | 82,077 | 84 | 6 | 8.9 s | 43 s | 158 MiB | 2,022 B | 29 s | ||
| Xenium Prime 5K ovarian | 130,000 | 4,603 | 6 | 73 s | 120 s | 353 MiB | 2,844 B | 83 s | ||
| Xenium pancreas | 130,001 | 376 | 6 | 8.3 s | 42 s | 187 MiB | 1,512 B | 36 s | ||
| CosMx WTx colon | 149,771 | 17,533 | 6 | 35 s | 144 s | 1.01 GiB | 7,264 B | — | ||
| STARmap cortex (3D) | 149,994 | 1,020 | 6 | 10.8 s | 55 s | 218 MiB | 1,521 B | — | ||
| osmFISH cortex | 149,997 | 35 | 6 | 8.1 s | 39 s | 169 MiB | 1,180 B | — | ||
| Xenium lung cancer | 550,000 | 377 | 6 | 50 s | 246 s | 393 MiB | 750 B | — | ||
| MERFISH ileum (3D) | 819,665 | 241 | 6 | 67 s | 340 s | 534 MiB | 684 B | — | ||
| Xenium Prime 5K slide, 1.0M subset | 995,906 | 4,358 | 8 | 152 s | 13.7 min | 843 MiB | 888 B | — | 23.1 min | 1.47 GiB |
| Xenium lung slide, 1.0M subset | 1,000,001 | 377 | 8 | 122 s | 579 s | 676 MiB | 709 B | 18.5 min | 14.0 min | 1.13 GiB |
| CosMx lung cancer | 1,967,437 | 960 | 6 | 243 s | 21.4 min | 1.07 GiB | 586 B | — | ||
| Xenium breast cancer | 1,991,578 | 321 | 6 | 138 s | 11.2 min | 1.06 GiB | 572 B | — | ||
| Xenium pancreas | 1,999,991 | 377 | 6 | 194 s | 16.1 min | 1.06 GiB | 570 B | — | ||
| Xenium Prime 5K ovarian | 1,999,997 | 5,088 | 6 | 242 s | 21.3 min | 1.06 GiB | 569 B | — | ||
| Xenium lung slide, 2.0M subset | 2,000,000 | 377 | 8 | 297 s | 24.3 min | 1.15 GiB | 620 B | 33.2 min | 30.1 min | 1.95 GiB |
| CosMx WTx colon slide | 2,969,193 | 18,935 | 8 | 19.5 min | 98.3 min | 2.16 GiB | 779 B | — | 257.0 min | 3.11 GiB |
| Xenium Prime 5K slide, 8.0M subset | 8,000,091 | 5,078 | 8 | 31.1 min | 161.3 min | 4.24 GiB | 569 B | — | 221.1 min | 6.87 GiB |
| Xenium lung slide (whole) | 10,605,483 | 377 | 8 | 63.2 min | 167.5 min | 5.32 GiB | 539 B | — | 194.9 min | 8.17 GiB |
Benchmark crops at 6 threads (median of the replicates) and profiling
ladders at 8 threads (one run), ordered by the number of molecules; click a
column header to sort. "Genes" is the panel size of the crop or slide subset.
The "before" columns are the code before the optimisation (e45fddc),
profiled for the ladders only. The datasets are described in the benchmark
repository's
DATASETS.md.
Threads¶
Figure 3. Wall time (left) and total CPU time (right) for 1–16 threads on
the 130k-molecule Xenium pancreas crop and the 820k-molecule 3D MERFISH crop
(log–log; one run per point, one process at a time, with
--skip-ncv-color as in the benchmark runs). The dotted lines are ideal
scaling from the 1-thread time. The vertical line marks the 8 physical
cores; 16 threads use hyper-threading.
8 threads are 6.0× faster than 1 thread on the Xenium crop (42.8 s → 7.2 s) and 5.8× on the MERFISH crop (346 s → 60 s), while the total CPU time stays nearly flat (+7 % and +11 %). Hyper-threads (16 threads) add about 20 %.
At real sizes 95 % of the CPU time is in parallel code (lung 2M: serial share 5.1 %, was 12.7 % before), and the thread pool hardly waits (≤ 0.4 % of the CPU time at 8 threads on rungs of 1M molecules and more). That bounds the speed-up at 5.9× on 8 threads and 9.1× on 16. The serial remainder is mostly graph construction, confidence estimation and polygons.
Small crops behave differently when the NCV colours are on: 62 % of the
instructions of a 20k-molecule crop run serially, most of them in the colour
embedding, so it speeds up only 1.38× on 8 threads (18.9 s → 13.7 s). Skip
the colours with --skip-ncv-color if you do not need them; the same kind of
data then scales as in Figure 3.
By default Baysor uses one thread per physical core (8 on this host). Set the
count with -t/--threads, the threads config key or OMP_NUM_THREADS; see
Threading.
Crop thread series (with the colour embedding)
Minimum wall time and median CPU time of 3 runs.
| crop | threads | wall time | CPU time |
|---|---|---|---|
| Xenium pancreas 20k | 1 | 18.9 s | 21.3 s |
| Xenium pancreas 20k | 2 | 15.9 s | 19.2 s |
| Xenium pancreas 20k | 4 | 14.4 s | 20.0 s |
| Xenium pancreas 20k | 8 | 13.7 s | 21.2 s |
| Xenium pancreas 20k | 16 | 13.8 s | 24.3 s |
| Xenium Prime 5K 20k | 1 | 14.5 s | 14.5 s |
| Xenium Prime 5K 20k | 2 | 12.5 s | 16.5 s |
| Xenium Prime 5K 20k | 4 | 12.1 s | 16.7 s |
| Xenium Prime 5K 20k | 8 | 10.2 s | 16.6 s |
| Xenium Prime 5K 20k | 16 | 11.0 s | 18.0 s |
Before the optimisation the 20k pancreas crop used 97 s of CPU time on 16 threads (OpenMP spin-waiting); now 24 s.
Gene panel size¶
Figure 4. Wall time (left) and peak memory (right) at 6 threads against the gene-panel size, for three families of simulations that differ only in the number of genes (64k–116k molecules each). On these simulations the panel size hardly matters: from 20 to about 5,000 genes the wall time stays at 3–9 s and the peak memory at 143–185 MiB. Above 3,000 genes the initialisation uses a truncated solver. Panels between 1,000 and 3,000 genes still take the dense initialisation, whose cost grows with the cube of the number of genes; on real data this is the slowest case per molecule, e.g. the 130k-molecule Xenium Prime 5K crop, whose 2,672 used genes go through the dense path, takes 73 s at 6 threads (Figure 1).
Reproduce¶
The charts, their data and the per-dataset table are regenerated from the
benchmark and profiling results with one command,
docs_figures/make_figures.py
in baysor-benchmarks; docs_figures/generated/tables.md there lists the
source file of each number. Per-function and per-line hotspots come from the
profiling suite
(profile.py for small crops, scaling.py for the ladders, report_html.py
for the HTML report). To benchmark your own build, see
Development › Benchmarks.