Skip to content

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 data (JSON)

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 data (JSON)

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 data (JSON)

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 data (JSON)

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.