← Back to Blog
2026-10-11 · Technical · Product page →

A three-language repeated benchmark of bioinformatics capabilities — Rust vs Python vs R over 8 capabilities at N=10

Linxira Bio SDK benchmark report · 2026-10-11. A three-backend N=10 repeated measurement of 8 capabilities on one low-end x86 machine: environment, input digests, scripts, tables and figures are all public and can be re-checked item by item. Changes and disclosures are listed in “Versions and disclosures” at the end.

Data status: all eight capabilities were re-run at N=10 in a window with no CI jobs (archive bench-20261011-001; the per-stage CI job count and loadavg are in window-log.txt). The comparison against the first batch (this host also ran a CI runner) is in §8.8: all 30 per-capability/per-backend rows fall inside the noise band, none beyond it, so no row was excluded. Tables and figures ship with this post (index and re-check recipes in §13).

1. Abstract

We measured 8 implemented capabilities of the Linxira Bio SDK on a low-end x86 machine (Intel Pentium Gold G7400, 2 cores / 4 threads, 15 GiB) across three language backends: the Rust engine, plus Python and R comparison arms built on Biopython and Biostrings. Every capability × backend combination got 10 timed repeats (preceded by one untimed warm-up), with fixed inputs and commands, serial execution, and an unchanged powersave governor.

Results use a two-part, pre-registered rule (§6.3):

Boundaries up front: this is a whole-stack comparison, and “whole stack” needs spelling out — the three arms implement the same algorithm and differ only in language runtime (same iterator, same decorrelation order, same convergence threshold). It therefore measures the language boundary, not “Rust vs the best available Python tool” (that is a different arm class; see the epilogue, §16). Every ratio holds only for this machine, these inputs, and this protocol (§11). One scope statement belongs in the abstract too: this host has 2 cores / 4 threads and all three arms’ critical paths were single-threaded (the engine uses no parallel facility today, CPython has the GIL, R’s evaluator is single-threaded), so this window does not exhibit Rust’s language-level parallelism — we state only what was measured.

2. Changelog relative to the two earlier posts

3. Background

Three posts form a line: ① a graduation project (a buckwheat genome) where only “does it finish” mattered; ② a one-off end-to-end reproduction of a public SRA dataset, followed by a dual-vendor GPU validation; ③ this one — no cherry-picked data, no cherry-picked metric, a protocol written down before the numbers were seen.

The motivation is concrete: the SDK’s promise is local-first. Local-first means users may well run it on a machine without a GPU and without spare RAM. A backend that is 20× slower and eats 10× the memory on such a machine is not a viable default. So what we need is not “Rust is faster than Python” but a per-capability, interval-carrying, reproducible comparison.

Comparison arms used in this window (third-party implementations, not ours):

BackendBasisVersion
Rustthis repository’s enginev1.1.3
Pythonmostly the standard library (csv/gzip/math) + Biopython (FASTA parsing) + pytrf (SSR only, a C extension)CPython 3.14.7 / Biopython 1.88 (measured; the pack lock pins 1.85 — mismatch logged) / pytrf 1.5.0
Rmostly base R + Biostrings (reader and its matching C kernels)R 4.6.1 / Biostrings 2.80.2 / Bioconductor 3.23

How the arms were constructed (stated explicitly here on 2026-10-12; previously it existed only in pack source comments). To make the language boundary the only variable, the three arms implement the same algorithm and therefore deliberately avoid ecosystem libraries that would replace it: prcomp, scikit-learn, gseapy, clusterProfiler, bio3d, Bio.PDB and phyper are all excluded, each with its reason written in the pack source and the capability docs (mainly: their sign conventions, object models or rounding differ from the engine, which would break the field-level comparison). Only two things were let through: the parsing layer (Biopython, Biostrings) and one C-extension arm (sequence.ssr’s pytrf). The numbers here therefore answer “does this algorithm’s critical path run in the interpreter or in native code”, not “Rust versus the best Python tool”. The latter is a different arm class (tool arms such as fastp/seqkit/pyfastx), not measured in this window — see the epilogue (§16).

None of the numbers here come from a third-party benchmark suite. Third parties appear in exactly two roles, neither of which is a suite: ① orchestrated tools (19 of the 36 entries in tools/catalog.json are analysis tools — bcftools, samtools, bedtools, ncbi-blast, diamond, hmmer, kraken2, salmon, minimap2, muscle, trimal, meme, iqtree, mcscanx, snpeff, mkdssp, kaks-calculator, autodock-vina, meeko) — these are implementations of capabilities, not arms; ② libraries used inside arms (Biopython, Biostrings, pytrf) — libraries, not suites. So this article carries no PTS/OpenBenchmarking, SPEC CPU or GIAB+hap.py numbers; those are external anchors we have planned and not executed in this window — see the epilogue (§16).

No third-party benchmark suite was used in this window. Two external calibration paths are planned and not executed, and are therefore not evidence here:

  1. GIAB + hap.py — to calibrate correctness (not speed) against a public truth set. Our variant-calling and comparison capabilities can consume those inputs, but the truth-set download, confident-region masks and containerised execution did not happen in this window.
  2. A public hosted benchmark platform (e.g. Phoronix Test Suite / the OpenBenchmarking result library) — to anchor this machine’s absolute numbers to a third-party machine lineage. Also not run.

We list both as “planned” rather than “done”, because writing something into a post does not make it done.

5. Data

Every input is produced by a committed generator script (generate-inputs.py): synthetic tables from random.seed(42), sequence inputs from the engine’s own seeded simulator (--seed 42). Anyone can rebuild the exact same bytes.

CapabilityInputSizesha256
sequence.stats.v1FASTA (20 × 5 Mb)~100 MB9f8a4cb46eb11eca49ad40108648541d13d03a50a79889a90977fe505c044e78
expression.pca.v1TSV matrix2000 × 40273755e5eca0ae14d150365271dda778666109341ec0826baa01b7dc7bd76345
set.venn.v1TSV set table20k items × 4 sets484dffd240e46a4de6995ae935107e464185ca93c9a8025e79856e1fa5234531
enrichment.overrepresentation.v1genes.txt5k genes84ac1b6eec0a96798aa39b905c654faa6156dc4f4af4d4eb5f672faf8d1f98db
enrichment.overrepresentation.v1associations.tsv45k associationsb523c5b4509487186461e8bc29adcdce910551ed1c4b88d4768c220c5ff9d781
structure.pdb.summary.v1PDB3000 residues / 24k atomsecb30a5181b376760ceab330d9244f79b28195b54a4a8cac26a8ca23e2559afe
variant.stats.v1VCF200k variants1dab9c47535f9b3552cd3cfae1fa70ac4fccc5ff7518d4c9cf5b76950861ce53
fastq.qc.v1FASTQ200 MB / 646k reads5697241ba2c6da89e89bd860eee8b18ba370e2725e63a2b4ccda9339fed13fb9
sequence.ssr.v1FASTA~100 MBsame file as sequence.stats (big.fa; sha256 9f8a4cb4…44e78)

Cross-machine, cross-version check: generating those six synthetic inputs on Windows (Python 3.13.15) and on the measuring machine (Linux, Python 3.14.7) yields six identical sha256 digests — the inputs do not depend on the generating machine.

6. Methods

6.1 Measurement protocol

6.2 Statistics

Median-of-10, inter-quartile range, coefficient of variation, min/max, peak RSS; backend ratios are ratios of medians, with a bootstrap 95% confidence interval over the raw wall_ms values (10,000 resamples, fixed seed 20261011).

6.3 Pre-registered decision rule (rule first, numbers second)

ConditionWording
ratio CI lower bound ≥ 1.10material: “N× faster (CI lower bound …)”
CI excludes 1.00 but lower bound < 1.10detectable-immaterial: resolvable, not practically meaningful
CI crosses 1.00within-noise: “no resolvable difference on this input and machine”

Rule-change record: we first used “2 × (CV₁ + CV₂)” as a noise-band threshold, then found it labels a consistent 2.4% gap as noise and a lucky 15% gap as signal — because dispersion is not an effect size. Hence the two-part rule above: resolvability from the CI, materiality from the 1.10 floor. CV is still reported per row, but no longer decides anything.

6.4 Trimming robustness

Besides all 10 repeats, medians and verdicts are recomputed after dropping the first repeat (9) and after keeping only the middle 8 (Tables S2 and S3). All three bases agree on every verdict (0 of 16 rows change); the medians themselves do move by roughly one 10 ms timer step (14 of 24 rows — itemised in Table S2). No conclusion depends on keeping a particular run, but “the medians are identical” is not true. Corrected 2026-10-12: the first version claimed the medians agreed too.

7. Environment

ItemValue
OSCachyOS (Arch family), native Linux filesystem (not a drvfs mount)
CPUIntel Pentium Gold G7400, 2 cores / 4 threads
Memory15 GiB
GPUnot used by this benchmark (a separate GPU experiment carrier exists, out of scope here)
Rustrustc / cargo 1.99.0
PythonCPython 3.14.7; numpy 2.5.4, Biopython 1.88, pytrf
R4.6.1; Bioconductor 3.23, Biostrings 2.80.2, jsonlite 2.0.0, digest 0.6.39
Governorpowersave (unchanged throughout)
Storageroot on btrfs / 512 GB NVMe; /tmp is tmpfs (7.7 GB), so every benchmark input is RAM-backed — no disk latency enters the measurement (two spinning disks are present and unused)
TimingGNU time, high-precision mode
Enginev1.1.3

This is a low-end machine: 2 cores / 4 threads, no turbo headroom, powersave. Its value is not representativeness but being a conservative floor: on faster hardware the constant terms (interpreter start-up, large allocations) get amortised differently, so ratios may grow or shrink. Every ratio here is therefore stated for this machine only.

8. Results

8.1 Three-backend medians and verdicts (N=10)

Full statistics in Table S1 (§13 lists every supplementary table and figure). Excerpt:

Capabilityrustpythonrpython verdictr verdict
sequence.stats.v1810 ms825 ms1,815 ms1.02× [1.01, 1.11] detectable, immaterial2.24× [2.23, 2.30] material
expression.pca.v120 ms4,010 ms2,770 ms200× [133, 208] material138× [92.3, 140] material
set.venn.v110 ms180 ms11,075 ms18.0× [18.0, 19.0] material1108× [1090, 1128] material
enrichment.overrepresentation.v130 ms230 ms4,480 ms7.67× [7.33, 7.67] material149× [149, 157] material
structure.pdb.summary.v130 ms780 ms20,680 ms26.0× [26.0, 30.0] material689× [683, 692] material
variant.stats.v180 ms760 ms13,500 ms9.50× [9.50, 9.56] material169× [164, 174] material
fastq.qc.v11,570 ms49,345 ms702,190 ms31.4× [31.1, 31.8] material447× [444, 447] material
sequence.ssr.v11,600 ms2,280 ms2,211,405 ms1.43× [1.33, 1.45] material1382× [1290, 1391] material

(An earlier N=3 window measured the same capabilities with the same conclusion; see §9 and the engineering ledger docs/engine-evals/local-nas-g7400-window-2026-10-04.md.)

Absolute wall time per backend (linear scale, ms)

Figure: absolute median wall time per backend (linear scale) — same input, same machine, 10 timed repeats per backend.

8.2 Memory (peak RSS, median-of-10)

Capabilityrustpythonr
sequence.stats.v15.9 MB69.6 MB (11.8×)321.1 MB (54.5×)
expression.pca.v18.5 MB53.4 MB (6.3×)81.9 MB (9.6×)
set.venn.v18.3 MB47.3 MB (5.7×)124.7 MB (15.1×)
enrichment.overrepresentation.v115.0 MB61.0 MB (4.1×)145.4 MB (9.7×)
structure.pdb.summary.v128.4 MB66.3 MB (2.3×)162.8 MB (5.7×)
variant.stats.v16.2 MB45.9 MB (7.4×)122.3 MB (19.8×)
fastq.qc.v16.2 MB46.0 MB (7.4×)79.4 MB (12.8×)
sequence.ssr.v121.0 MB150.7 MB (7.2×)667.5 MB (31.8×)
Peak resident memory per backend (linear scale, MB)

Figure: peak resident memory per backend (median-of-10, GNU time’s VmHWM).

Peak-RSS multiplier of the interpreter backends against Rust

Figure: memory multipliers — 2.3×–11.8× on the Python arm and 5.7×–54.5× on the R arm; the only row below 2× is structure.pdb.summary/python (2.3×) and the largest is sequence.stats/r (54.5×).

8.3 Repeat stability and ratio intervals

Raw wall-time spread of the 10 repeats, with each backend's median

Figure: every recorded wall_ms for the 10 repeats (the vertical bar in each row is that backend’s median) — a check that no median is produced by a single outlier run. Most medians sit within ±1 IQR across repeats. sequence.stats — Rust’s granularity is 10 ms (the timer’s resolution), so its CV (1.95%) is dominated by quantisation, not real jitter. That limits resolution on small absolute values and is one reason the materiality floor is 1.10 rather than 1.01.

Relative spread across the 10 timed repeats (CV)

Figure: repeat dispersion — four Rust rows are bit-identical across all 10 repeats (CV 0.00%) and the highest value is 21.52% (expression.pca, whose median is only 20 ms, so the timer’s step dominates); the Python arm spans 0.75%–8.97% and the R arm 0.08%–5.93%. Dispersion is stability evidence and takes no part in the ratio verdict.

Ratios with bootstrap 95% confidence intervals (linear scale)

Figure: the ratio with its 95% CI for every backend pair; the dashed lines are 1.00 (no difference) and 1.10 (the materiality floor), and each label carries the verdict.

8.4 Trimming robustness

All three bases agree on every verdict (0 of 16 rows change); the medians move by about one 10 ms timer step (14 of 24 rows — see Table S2). In other words: no conclusion survives only because a particular run was kept.

From one capability measurement to a verdict: protocol and decision rule

Figure: the whole chain — fixed, digest-checked inputs → 1 untimed warm-up → 10 timed repeats → median/IQR/CV → bootstrap confidence interval → three-way verdict. The warm-up never enters the report; the trimming bases are robustness checks only.

8.5 The record-granularity experiment: a falsified hypothesis

Original hypothesis: sequence.stats differs from Python by only 2% on million-base inputs because “a language boundary is crossed once per record”; more records should widen the gap.

Experiment: the same ~100 Mb, in three record shapes, N=10 each.

Input shaperustpythonr
1 × 100 Mb (one record)810 ms850 ms1,820 ms
20 × 5 Mb810 ms835 ms1,820 ms
100,000 × 1 kb820 ms860 ms1,930 ms

Falsified. Going from 1 to 100,000 records moves Rust by 10 ms and Python by 10 ms (ratio 1.037 → 1.036). At ~100 MB both sides are dominated by I/O and large allocations: the per-base work lives in C primitives, while reading and building each line costs both sides.

Same ~100 Mb, three record shapes: medians per backend

Figure: the record-shape experiment — the total is constant and the record count goes from 1 to 100,000 while all three lines stay nearly flat, which is the direct evidence that falsified the original hypothesis.

Corrected explanation (which is also the layering used throughout this post): the dividing line is not record count but “does the interpreter touch each element?”. Capabilities whose per-base work lives in C (sequence.stats) land at ≈1×; capabilities whose per-item/per-record work lives in the interpreter (set.venn, pdb, variant, enrichment, fastq.qc) span 7.67× (enrichment’s Python leg) to 1108× (set.venn’s R leg). The ~20× seen on tiny inputs is interpreter start-up (~170 ms) landing on a 10 ms workload — not throughput.

What we expected, and what the measurement refuted. The first intuition was that shuttling data across the language boundary would cost real time — Python reads the FASTA, hands the sequence to the statistics, receives the result back, and every crossing should add overhead. The data says no: the hot loop on the Python side is not in the interpreter but in CPython’s C primitives (str.count, str.upper, …). Biopython’s parser does step through Python per line, but the per-line work is tiny, and the large allocations plus file reading that both sides must pay for absorb the difference. So the 1.02× on sequence.stats is not “Rust has no advantage”; it means both stacks execute the critical path in native code — the first layer of the layering. Conversely, as soon as each element’s work must run in the interpreter (per-base in fastq.qc, per-item in set.venn), the same boundary costs 31.4×–1382× (§8.6).

8.6 Layered overview

Layered overview: Rust's wall-time ratio against both arms (linear scale)

Figure: the layered overview — ratios sorted by layer, with C-library-covered loops on the left and per-element interpreter iteration on the right.

Sorted by the decision rule: which rows actually reach the 1.10× material line

Figure: the ladder from the decision’s point of view — 15 of 16 rows are ≥ 1.10×; the only exception is sequence.stats/python (1.02×). The span covers three orders of magnitude, so the axis is split into two linear panels (1–10× and ≥10×), and the decision line falls in the lower one.

LayerCapabilitypythonr
Linear scan covered by C primitivessequence.stats1.02× (detectable, immaterial)2.24×
Per-position scan (Python arm is pytrf, a C extension)sequence.ssr1.43×1382×
Per-base interpreter loopfastq.qc31.4×447×
Per-item interpreter loop (small in-memory objects)set.venn18.0×1108×
Per-gene / per-variant interpreter loopenrichment, variant7.67× / 9.50×149× / 169×
Per-atom interpreter loopstructure.pdb.summary26.0×689×
Same hand-written power iteration in all three arms (no linear-algebra library)expression.pca200×138×

Note the expression.pca row: Python is slower than R there (200× vs 138×) because both arms run a all three arms run the same hand-written power iteration (500 iterations + Gram-Schmidt decorrelation; prcomp and scikit-learn are deliberately excluded — see §4), so this row’s gap comes not from a library but entirely from the runtime boundary: the same scalar loop executed by an interpreter (Python/R) versus in native code (Rust). Python is slower than R here (200× vs 138×) because of interpreter loop overhead and pure-Python row-by-row csv parsing, not because one numeric library is better. Corrected 2026-10-12: the first version said both arms used vectorised libraries (numpy vs prcomp), which the code contradicts.

8.7 Correctness evidence (cross-backend consistency)

Every benchmark report carries a three-backend consistency verdict: all 8 capabilities are consistent within a 1e-6 relative tolerance, precision class high. Additionally, cross-machine determinism was checked: generator outputs hash byte-identically on the NAS and on a second machine (ref.fa, cell.tsv, cell.json). Table S5.

Precision comparison against what the earlier window recorded (each window’s own evidence, shown side by side, never mixed):

CheckEarlier window bench-20260913-001This window (N=10)
Backend consistency verdictConsistent (4 capabilities)consistent (8/8 capabilities)
Consistency tolerance1e-6 relative1e-6 relative (same default)
PDB per-field deviation0.0 (identical)findings empty (no differing field paths)
PCA / Venn max relative error≤ 2.26e-11recorded as a verdict: consistent, no differing fields
Precision classhighhigh
Cross-machine determinismnot recordedyes: ref.fa / cell.tsv / cell.json sha256 identical across machines

Stated honestly: the earlier window recorded numeric parity errors (2.26e-11), while this window records verdict-level evidence (within 1e-6, no differing fields). Neither substitutes for the other, so the table shows both bases rather than moving “0.0 / 2.26e-11” under this window’s name.

8.8 Condition comparison: before and after the CI co-tenant (same protocol, same machine)

This host also runs a local CI runner. In the first batch two jobs landed inside the R leg of fastq.qc; with the jobs stopped we re-ran the whole window under the same protocol (window-log.txt records the CI job count as 0 at every stage boundary, jobs=0). The two batches, per capability and per backend (condition-comparison.tsv, 30 rows):

RowContended batchClean batchΔNoise band
sequence.stats / python840 ms825 ms−1.79%±10.47%
sequence.stats / r1,840 ms1,815 ms−1.36%±20.28%
sequence.stats / rust820 ms810 ms−1.22%±4.67%
fastq.qc / r702,655 ms702,190 ms−0.07%±1.92%
Condition comparison: each row's shift divided by its own band

Figure: the signal-to-noise view of the condition comparison — all 30 rows sit below |Δ| ÷ band = 1 (the maximum is 0.26, sequence.stats/rust: −1.22% against ±4.67%), so no row is beyond its band and none had to be excluded; the raw Δ% and ±band are printed on every row.

9. Comparison with the September window

Same capabilities, same commands, different input sizes and window (Table S4, generated from the 2026-09-13 archive; Table S4):

CapabilitySeptember window (N=5, small fixtures)This window (N=10, large inputs)Ordering consistent?
sequence.stats40 / 330 / 2110 ms810 / 825 / 1,815 msyes (rust ≤ py < r)
expression.pca40 / 300 / 460 ms20 / 4,010 / 2,770 msyes
set.venn40 / 310 / 400 ms10 / 180 / 11,075 msR moves drastically with scale
structure.pdb.summary40 / 320 / 410 ms30 / 780 / 20,680 msyes (magnitudes unfold with size)

Why only four rows: the table lists only capabilities that have an archived three-backend row in both windows (values in Table S4). enrichment.overrepresentation, variant.stats, fastq.qc and sequence.ssr have no three-backend row in the September archive, so they are not listed — putting differently-scoped numbers side by side is what makes a comparison untrue. For variant.stats the only earlier number on record is a single-repeat Windows end-to-end run (rust 42 ms / python 301 ms, 2026-09-19, no r row), not comparable with this window’s 200k variant input; it is recorded in the repository benchmark records (2026-09-19 entry).

Same capability, two windows: ratios on small fixtures and on large inputs

Figure: the same capabilities in both windows (small = September fixtures, large = this window). Small fixtures flatten all three backends, so their ratios are larger; real throughput only shows at scale.

Reading: small fixtures flatten all three backends (tens to hundreds of ms) because start-up and constant terms dominate; magnitude differences only unfold on large inputs. The R arm of set.venn looks “fine” on the small fixture (400 ms) and becomes 11,075 ms at scale — consistent with §8.5.

For identical inputs, cross-window absolute numbers agree to <3% median difference, so this machine’s measurements are themselves reproducible; the rows that must not be compared directly are the ones with different input sizes (flagged above).

10. Engineering findings: what running the R arm for real exposed

The real product of a three-backend measurement is that it forces execution. The R backend had never run its tests in CI; wiring it up exposed six defects in one pass, all fixed and merged:

#SymptomRoot cause
1SSR capability errored outBiostrings defines width() only for XStringSet, not for a bare XString
2Test literal disagreed with its assertionThe “22 bp” literal was actually 23 characters
3SSR always returned 0 motifsstart(...) without a namespace prefix resolved to stats::start (time series)
4All of the above stayed hiddenCI never ran the benchmark packages’ tests (no steps at all)
5Inputs without SSR were rejected by the workerAn empty list() serialised to []; the contract requires a map ({})
6Wrong intersections count for 20k-item inputGenerated per item instead of grouped by occupancy mask

A product-level defect surfaced the same way: looks_like_sam classified a wide count matrix as SAM based only on “≥11 columns plus numeric-looking fields” (measured boundary: 10 columns → tsv, 11 columns → sam), so any expression matrix with ≥11 samples was rejected by the worker. It now demands structural SAM evidence (FLAG bound, non-numeric RNAME, CIGAR grammar, base alphabet for SEQ) and carries a regression test.

One lesson: a test that exists but never runs in CI does not exist.

11. Limitations and honest boundaries

12. Reproducing

The whole chain for this window — staging the reports, input digests, summary, statistics tables, figures and the figure check — is one committed script, archive.sh, with the window descriptor and the step scripts beside it. Scripts and archive live at https://github.com/Linxira-OS/linxira-bio-sdk; the commands below assume the repository root as the working directory. Step by step:

# 1) inputs (seeded; writes only to the target directory)
python scripts/benchmark/inputs/generate-inputs.py --set synthetic --out /tmp/lxn10
python scripts/benchmark/inputs/generate-inputs.py --set sequence  --out /tmp/lxn10 --binary target/release/linxira-bio
python scripts/benchmark/inputs/generate-inputs.py --set reads     --out /tmp/lxn10 --binary target/release/linxira-bio
python scripts/benchmark/inputs/generate-inputs.py --set granularity --out /tmp/lxn10

# 2) repeated measurement (1 untimed warm-up + 10 timed repeats per backend)
linxira-bio benchmark run <capability> --input <file> --backends rust,python,r --repeat 10 \
  --report /tmp/lxn10/out-<capability>/report.json

# 3) archive (schema-checked) and statistics tables
python scripts/benchmark/analysis/summarize-reports.py --reports-dir /tmp/lxn10 --descriptor descriptor.json \
  --output-dir benchmark-results/2026-10-11
python scripts/benchmark/analysis/statistics-tables.py --summary benchmark-results/2026-10-11/summary.json \
  --reports benchmark-results/2026-10-11 --output-dir benchmark-results/2026-10-11

# 4) figures (self-contained SVG)
python scripts/benchmark/figures/render-svgs.py --summary benchmark-results/2026-10-11/summary.json \
  --reports benchmark-results/2026-10-11 --output benchmark-results/2026-10-11/blog-mirror/figures

13. Data and code availability

Supplementary index (the body cites these by label)

LabelFileContents
Table S1statistics.tsvper capability × backend: N, median, IQR, CV, min/max, peak RSS, ratio, 95% CI, verdict
Table S2statistics-trimmed-medians.tsvmedians and CVs under the three trimming bases
Table S3statistics-trimmed-verdicts.tsvratios, CIs and verdicts under the three trimming bases
Table S4statistics-comparison.tsvcross-window comparison against the 2026-09-13 archive
Table S5statistics-consistency.tsvcross-backend consistency, tolerance, precision class
Figures F1–F12blog-mirror/figures/<lang>/method and decision rule, absolute time, memory (absolute and multiplier), ratio CI, raw repeats, dispersion, record shapes, layering, decision ladder, two-window comparison, condition signal-to-noise

How a third party can re-check this (everything is in the repository)

What you want to checkHow
Inputs are reproducibleRun generate-inputs.py and compare byte-for-byte with the archived inputs.sha256
Figures agree with the tablesRun verify-figures.py: it maps every plotted number (including all 210 dots of the raw-repeat figure) back to the archived wall_ms values and exits non-zero on any mismatch
Figures were not smoothed or axis-truncated for effectEvery caption carries the “no smoothing, no axis truncation, no statistical transformation” disclosure, and the checker also asserts that disclosure is present and that no caption line overflows the canvas
The engine uses no parallelism todaygrep -rn "rayon|thread::spawn|par_iter" engine/crates/ returns nothing (this window’s check)
Can the rerun and the condition comparison be checked?condition-comparison.tsv (30 per-capability, per-backend rows) plus window-log.txt (CI job count and loadavg at each stage boundary), both in the archive
The whole window in one commandarchive.sh (stage reports → input digests → archive → statistics → bilingual figures → figure check)

14. Discussion and outlook

14.1 Discussion: what the layering explains

Read eight capabilities on one sheet and a stable pattern appears: Rust’s advantage tracks where the per-element work happens. When the critical path already runs inside a C library or a vectorised numerical library — per-base counting in sequence.stats, the matrix factorisation in expression.pca — the three arms collapse to 1.0×–2.2×, and the Python arm of sequence.stats is not only 1.02× away (CI lower bound 1.01, short of the 1.10 material threshold). When every element has to pass through the interpreter — per record, per item, per atom, per base — the gap unfolds as 7.7× → 26× → 31×, with the R arm reaching 149×–1382×. This is not a language ranking; it is a function of the workload’s shape. It predicts whether a given piece of code spends its time in an interpreter or in native code, and nothing more.

Three practical consequences follow. First, if the bottleneck is a linear scan over a large file, the three arms may genuinely be indistinguishable, and the interesting differences become memory footprint and start-up cost — where Rust leads consistently here (the two interpreter arms use 2.3×–11.8× and 5.7×–54.5× the peak resident memory). Second, if the bottleneck is a loop that makes a small decision per element — quality control, set operations, annotation parsing, variant traversal — the difference becomes one to three orders of magnitude. Third, once a third-party numerical library is involved, the language difference is masked by library quality: expression.pca is the example, where both arms run vectorised libraries and the 200× versus 138× gap comes from the library and its calling conventions rather than from the language.

14.2 Outlook: multi-core, GPU, and the virtual cell

One of this window’s most important results is a scope result: we measured a single core, and Rust’s language-level parallelism has not appeared yet. Presenting what was not measured as an advantage would be dishonest, so the next steps are stated plainly.

Multi-core comes first. The engine’s CPU-bound paths use no parallel facility today, and decompression of compressed inputs does not overlap with computation — which means that on terabyte-scale gzip data its distance from native Python/R is absorbed by IO and decompression (Python’s gzip is equally single-threaded, so this item currently buys no separation either way). The decided direction is: parallel decompression (block-parallel BGZF reads, a SIMD decompression library for plain gzip), a pipeline that overlaps decompression with computation, Rayon for multi-core work, and Tokio for IO concurrency across large file and process fan-outs. This window’s 1.02×–1382× are single-core numbers; once parallelism lands, the gap for very large sequence comparison and multi-trajectory simulation should widen with the core count. The measurement will then extend the same protocol with a core-count scaling curve (parallel efficiency and the knee point) and record the frozen thread count for every measurement point, so that a reader can tell an algorithmic gain from a core-count gain.

Vendors are not equally far along in Rust-for-accelerator support (re-checked 2026-10-12; sources in §17). Two things are routinely conflated: calling a GPU from Rust and writing a kernel in Rust. The four vendors sit in completely different places on those two questions.

Our own execution record needs the same distinction: what has been run is “one C/C++ kernel source, two native toolchains” — the same kernel suite (copy/triad/histogram/Pearson) compiled by nvcc (RTX 4080S, sm_89) and mcc (MTT S4000), median-of-3, passing every correctness gate, with per-kernel ratios anyone can recompute (source in §17). The Rust-kernel step has not been run: cutile-rs was not on crates.io at the time, and its dependency chain plus driver gates kept it out of execution; Moore Threads’ cudarc-musa host layer exists, but we have never measured it (our gpu-musa backend is still a stub). “One Rust source, two vendors” is therefore a route awaiting verification; what is established is single-source kernel portability. What remains is toolchain maturity, not architecture.

Why the virtual cell needs both. Our mechanism-level cell simulation (exact Gillespie, transcriptional bursting, Monod growth, binomial division) is single-threaded today as well. Its parallel axis is intrinsic: trajectories, parameter points and cells are mutually independent — embarrassingly parallel — so once parameters are swept or populations are simulated, the core count directly bounds the study size. Going further, folding omics-scale computation in (which is where “AI virtual cell” work actually spends compute) makes GPUs a necessity, and Rust now has official entry points on two vendors’ GPU stacks.

What these numbers are for. This window’s single-core numbers become the baseline for the parallel version: same machine, same inputs, same protocol is the only way to report “what parallelism bought” separately from “what the language or implementation difference buys”. Parallel implementations will follow the version-coexistence route and pass the full acceptance path again (contract, fixtures, skill documentation, bilingual docs); no timeline is promised here.

15. Questions and answers

Q1. Is Rust’s lead simply an artefact of comparing against interpreters? Not exactly, and the framing is refuted by the data: sequence.stats differs by 1.02× (interval CI [1.01, 1.11], excluding 1.00 but below the 1.10 material threshold) because that capability’s per-base work happens in C primitives; interpreter cost appears only where every element must pass through the interpreter. The advantage follows the workload’s shape, not the mere fact that the other arms are interpreted.

Q2. Were the comparison arms written sub-optimally, inflating the gap? First, a correction to a common misreading: the arms are not “each language’s idiomatic library code”; they are the same algorithm written in three languages (parity arms), with algorithm-replacing libraries deliberately excluded (§4). So they were neither deliberately slowed down nor are they Python’s best case: they measure the language boundary. Switching to each ecosystem’s standard tools (fastp/seqkit/pyfastx/scikit-learn) would move the ratios substantially — that is exactly what the epilogue (§16) commits to doing next. We make no blanket claim that “Python is slow”.

Q3. With a single core, is Rust’s real advantage simply absent? Yes — and that is stated in the abstract and in §11: the engine contains no parallel facility today, CPython is constrained by the GIL, and R’s evaluator is single-threaded; on a 2-core/4-thread host the parallel advantage cannot physically show up. We do not count unmeasured effects as advantages, and multi-core is the first item of §14.2.

Q4. Are 10 repeats enough to support the conclusions? The decision rule was fixed before the numbers were seen (resolvable only when the interval excludes 1.00; material only when its lower bound is ≥ 1.10); all three trimming bases agree; all 10 raw wall times per backend are published; and for the one capability where nothing is resolvable we write exactly that. That is itself evidence of statistical discipline.

Q5. Why only one machine and one input scale? A single host and a single scale are this window’s deliberate scope; the inputs are 100–200 MB, far above start-up magnitude. Cross-machine anchoring (a public hosted benchmark platform) and correctness truth sets (GIAB with hap.py) are planned, and are not evidence until they are executed.

Q6. Is R’s ~37-minute SSR leg a deliberately slow library choice? That leg uses Biostrings’ matchPattern, the standard approach in that ecosystem, whose cost grows with the input and the motif space; we report the absolute time (N=10 median 2,211,405 ms, about 36.9 min per run) and explain that this is an algorithmic property, not a tuning artefact. The Python arm of that capability uses pytrf, a C extension (not pure Python), which is why its gap is only 1.43× — a direct illustration of what “whole-stack comparison” means: small against a C extension, three orders of magnitude against R’s per-position interpreter loop.

Q7. Is the memory comparison fair? Same tool (GNU /usr/bin/time -v’s VmHWM), same definition of the instant, median over 10 repeats; and the post discloses that resident set size is not the whole memory footprint (shared libraries and mapped files are attributed differently).

Q8. Why no third-party benchmark suite? The comparison arms here are third-party library implementations, which is a different thing from a third-party benchmark suite; GIAB with hap.py (correctness truth sets) and a public hosted benchmark platform (machine-lineage anchoring) are planned and are not evidence until executed (§4, §11).

Q9. How can a reader confirm that the figures match the tables rather than having been tuned? Figures and tables are generated by one script from one archive; a verification script re-derives every plotted number — including all 210 points of the raw-repeat figure — from the archived values and fails on any mismatch; each caption carries the “no smoothing, no axis truncation, no statistical transformation” disclosure, and axes are linear with the truncated range printed on the figure. See §13 for how to re-run the checks.

16. Epilogue: tomorrow’s second pass — replacing “the same algorithm” with “each language’s standard practice”

After writing this up we re-read our three arms line by line and found one thing that has to be said in the open — and it decides the next step.

This window’s arms are “one algorithm × three languages”, not “three languages’ idiomatic code”. To collapse the variables down to the runtime boundary, all three arms implement the same algorithm (same iterator, same Gram-Schmidt decorrelation order, same convergence threshold) and deliberately exclude the ecosystem libraries that would replace it (prcomp, scikit-learn, gseapy, clusterProfiler, bio3d, phyper; reasons in §4). The only external native code let through is the parsing layer (Biopython/Biostrings) and one C-extension arm (sequence.ssr’s pytrf).

That is what bounds these numbers: they answer “does the same algorithm’s critical path run in the interpreter or in native code”, not “Rust versus the best Python tool”. The counter-example is inside this very article: with the C extension pytrf on the Python arm, sequence.ssr’s gap collapses from the 31× class to 1.43×.

Tomorrow (2026-10-13) we re-run with more standard code, adding “each language/ecosystem’s standard practice” as a second arm class (tool arms): scikit-learn and prcomp for expression.pca, pyfastx/seqkit for sequence.stats, fastp and pyfastx for fastq.qc, and ecosystem-standard implementations elsewhere, labelling per row whether the algorithm is equivalent.

The same pass adds the third-party anchors that had been “planned, not executed” — now scheduled: ① lzbench and Squash for compression (single-threaded reference numbers plus fixed-iteration semantics); ② the PTS compress-gzip/compress-zstd/compress-7zip profiles (quoted with profile revision and run command), with results uploaded to OpenBenchmarking for machine-lineage anchoring (carrying PTS’s own comparability caveat: same version, same hardware, same day); ③ GIAB HG002 + hap.py for correctness truth, stated separately from any speed claim. Note that PTS still has no bgzip profile, so only part of what we measure has a ready-made third-party counterpart; where it does not, we say so rather than substituting a different workload.

The two classes are measured separately, tabulated separately and never mixed:

The protocol does not change (digest-fixed inputs, 1 untimed warm-up + N=10 timed repeats, median + bootstrap CI, the two-part verdict, trimming robustness, condition comparison), so the two passes can be read side by side — but the two arm classes must not be compared against each other. That is the most important self-correction of this round: first be clear about what we actually wrote, then talk about speed.

The same pass fixes three “lock versus environment” mismatches found while re-reading the code (reproducibility defects, not wrong numbers): ① the Python pack’s requirements.lock pins biopython==1.85/numpy==2.2.4 while the archived environment reports Biopython 1.88 / numpy 2.5.4 — the lock did not govern the run; to be unified next pass; ② expression_pca_v1.py’s docstring claimed numpy.linalg did the arithmetic (it never did) — corrected; ③ the roadmap described the arms as “ecosystem benchmark tools”, contradicting the parity arms that actually shipped — now a two-class rule in §7.1.

17. References

  1. NVIDIA. CUDA Rust platform (cuda-oxide, cutile-rs and the runtime crates): https://github.com/NVIDIA/cuda-rust; announcement: Introducing CUDA Rust: Two Tracks for Writing GPU Kernels, 2026-09-08: https://developer.nvidia.com/blog/introducing-cuda-rust-two-tracks-for-writing-gpu-kernels
  2. Intel. openvino-rs (official inference bindings, CPU/GPU/NPU): https://github.com/intel/openvino-rs; oneapi-rs (sycl-rs, experimental; kernels are C++ SYCL source strings): https://github.com/oneapi-src/oneapi-rs; ittapi (instrumentation): https://github.com/intel/ittapi
  3. AMD. Rust repositories in the official ROCm organisation: https://github.com/ROCm/spur, https://github.com/ROCm/rocm-cli, https://github.com/ROCm/amdsmi (contains the official rust-interface/); third-party Rust-kernel route: CubeCL: https://github.com/tracel-ai/cubecl
  4. Moore Threads. cudarc-musa (officially maintained fork; feature musa-50200 = MUSA 5.2 driver ABI): https://github.com/MooreThreads/cudarc-musa; sppark-musa (the musa feature of the sppark crate): https://github.com/MooreThreads/sppark-musa
  5. Our own dual-vendor kernel measurements (one C/C++ kernel source through two native toolchains, nvcc and mcc, on an RTX 4080S and an MTT S4000): https://github.com/Linxira-OS/linxira-bio-sdk/blob/main/docs/engine-evals/gpu-rental-round3-dual-vendor-2026-10-02.md
  6. Our re-check ledger for vendor Rust support (2026-10-12; includes the four-vendor three-layer matrix, the search method and the development direction in §8): https://github.com/Linxira-OS/linxira-bio-sdk/blob/main/docs/engine-evals/vendor-rust-status-2026-10-12.md
  7. Software versions used throughout are disclosed in §7; Biopython, Biostrings, numpy, R and CPython are cited there as their own project releases.

18. Versions and disclosures


Linxira-OS · AGPL-3.0-or-later (code) · this article CC-BY-4.0 · https://github.com/Linxira-OS/linxira-bio-sdk