HORUS/blog

Jul 10, 2026 · benchmarks · performance · ros2

We pulled our headline ROS 2 speedup number. Here's the one we can defend.

The multiplier in our old headline was never measured. Here's the methodology, the ROS 2 comparison the repo can actually back, and how to run it yourself.

If you arrived here from the old headline — the one with the very large multiplier against ROS 2 — that number is withdrawn. This post used to lead with it, and it should not have.

Here is what was wrong with it, because the failure is more instructive than the number was.

The number we could not source

The old table put HORUS's nanoseconds next to ROS 2 latencies in the tens of microseconds and divided. The HORUS side was real. The ROS 2 side was not: those latencies appear in no benchmark, no report and no source anywhere in the repository. They were plausible-looking figures that nobody had measured and nobody could check.

Two smaller errors rode along with the first:

That last one is now fixed at the source: every BenchmarkResult carries a provenance field, and a figure quoted out of a paper is tagged literature, not measured.

The rest of this post is the part that was always true.

How the numbers are measured

Latency this small is easy to measure wrong. The harness does the following, and you can read every line of it in benchmarks/:

None of that is exotic — it's just what you have to do to report a nanosecond honestly. It is also, obviously, not sufficient: a rigorous measurement of one side proves nothing about the side you didn't measure.

What HORUS actually measures

On an Intel i9-14900K, with the topology and the measurement spelled out, because those two rows are not comparable to each other:

TopologyHORUSWhat is being timed
Same-process pub/sub91 nsproducer-side send()
Cross-process171 nsend-to-end, one-way
1 publisher → 3 subscribers80 nsproducer-side send()

The speed isn't a clever optimization of the serializer — it's the absence of one. HORUS moves messages through lock-free shared-memory ring buffers; the message is the type's memory layout, so there's nothing to serialize.

Against ROS 2, conservatively

The nearest published figure for ROS 2's default DDS is REP 2014's reference: roughly 5 µs median for a 64-byte same-process message. Compared against HORUS's end-to-end cross-process 171 ns — the harder case for HORUS, and therefore the conservative comparison — that is roughly 30x.

It is a quoted number, not one we ran, and it is doing a lot of work in that sentence: different machine, different kernel, different DDS vendor, one payload size. If the ratio matters to your decision, measure it on your hardware and your message sizes. Building the comparison with -F dds against an installed DDS implementation is the only way to get a measured one out of this repository.

Against iceoryx2, measured on both sides

This is the comparison worth arguing about — iceoryx2 is also shared-memory IPC, so it's a much fairer fight than DDS, and both sides run in the same harness:

HORUSiceoryx2Speedup
Same-thread11 ns69 ns6.3×
Cross-process170 ns361 ns2.1×
Throughput95 M msg/s22 M msg/s4.3×
cargo run --release --bin iceoryx2_comparison --features iceoryx2

A smaller number that a reader can reproduce is worth more than a large one they cannot. That is the whole lesson of this post's first draft.

It holds up under load, too: near-linear scaling to 100 nodes (14% degradation) and O(1) to 1,000 topics.

Run it yourself

You don't have to trust any table above:

cargo run --release -p horus_benchmarks --bin all_paths_latency

That prints the full percentile tables (p50 / p99 / p99.9 / max) for all ten IPC backends on your hardware, the backend selected for each topology, and the measured hardware floor it subtracts. If your numbers differ from ours, that's a bug report we want.

HORUS is open-source and Apache-2.0. If "reproducible or it didn't happen" is your bar too, the harness is all there in the repo — and so, now, is the provenance of every number next to it.

Found this useful? Share it:Discuss on HNShare on X