Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Benchmarks

Measured against libfranka 0.20.4 and libfranka main, with the harness in bench/: loop jitter against franka-sim over loopback, the model inside the 1 kHz loop, and the model calls offline.

Every measurement ran on one machine: a laptop-class x86-64 CPU with 12 logical cores, Linux 6.8, not PREEMPT_RT. That matters for how to read the tail: every multi-millisecond max in these tables is a scheduling event on a desktop kernel, and these numbers are a same-machine A/B, not an FCI qualification of either client.

The fairness protocol: a cell is one (condition, variant, repetition), both clients run back to back inside it, the order alternates per repetition, and the simulator container is restarted per cell. The paired within-cell difference is the primary evidence everywhere below; pooled tables are context.

Simulator: loop jitter

Joint-velocity motion, 30 s per run, 3 repetitions, chrt -f 80 + mlockall.

variantclientinterval p50p99lost cyclessuccess avgCPU %
control (callback)C++1000 µs1071 µs0.31.0002.7
control (callback)Rust1000 µs1050 µs0.31.0002.4
active (readOnce/writeOnce)C++1000 µs1046 µs0.01.0003.0
active (readOnce/writeOnce)Rust1000 µs1054 µs0.01.0002.4

Paired over all 18 cells:

statisticC++ minus Rustverdict
CPUmean +0.59 pp, C++ higher in 18/18 cellsreal
interval p99mean −15 µs, median −3 µs, C++ higher in 8/18noise
active recv→send latency p50 / p9917 / 34 µs vs 11 / 23 µs (9 cells)real

The CPU gap holds equally whether C++ ran first (+0.57) or Rust ran first (+0.63), so it is not an ordering artefact. Loop timing is indistinguishable: median cycle time is exactly 1000 µs everywhere and the p99 difference changes sign between variants. Under a 12-core CPU hog, p99 rises to 1.3–1.4 ms (control) or ~1.1 ms (active) for both clients, p50 stays at 1000 µs, and neither client lost a cycle.

The model in the loop

The same operational-space impedance controller in both languages, with all five franka::Model calls evaluated inside every 1 kHz cycle. Simulator, chrt -f 80 + mlockall, 3 repetitions of 30 s.

buildmodel p50p99compute p50interval p50p99lostCPU %
libfranka 0.20.430.8 µs67.033.3 µs1000 µs108405.7
libfranka main (0.21.3)31.2664.4433.961000104905.59
libfranka main + Data-reuse patch11.0727.1513.881000104303.49
franka-rs10.832.912.51000104702.9

Both clients produce the same motion (peak |τ| 1.925–1.940 Nm, peak end-effector excursion 0.04785–0.04789 m), and neither lost a cycle in any of the 12 runs.

The five calls measured offline, on 10 000 identical random FR3 states:

calllibfranka 0.20.4libfranka main+ patch commit 1+ commit 2franka-rs
mass0.670.630.620.610.68
coriolis (state overload)1.931.961.911.861.13
gravity0.570.600.570.580.57
zeroJacobian6.977.170.810.500.32
pose6.346.550.360.370.27
all five16.4816.934.283.922.97

p50 microseconds. The 21–24× gap on pose and zeroJacobian was not a C++/Rust difference: libfranka’s Pinocchio wrapper constructed a whole fresh pinocchio::Data on every kinematics call — 143 heap allocations. A two-commit patch (see patches/) takes that to zero allocations per call and closes essentially the whole gap; the remaining ratio is about 1.3× offline and 1.0× in the loop, and it is not all in the same direction (C++ ahead on mass, franka-rs on coriolis and the Jacobians). Every output is byte-identical before and after the patch, and libfranka’s own 805/805 tests pass on both.

Numerical agreement over the same 10 000 inputs: the largest absolute difference anywhere between the two backends is 4.97e-14 (zeroJacobian 9.99e-16, pose 1.06e-15, mass 3.55e-15). That is double-precision round-off.

Reading in-loop model timings

An in-loop model_us figure prices the CPU’s post-idle frequency ramp as much as the arithmetic: a duty-cycled loop starts each cycle on a core that has just idled. The five FER model-library calls in C++ (bench/so-micro), timed after increasing amounts of untimed filler work, swing from 21.37 µs to 3.98 µs on identical inputs. So a client whose receive path does less work can read slower in the loop; compare the arithmetic with the offline table.

Reproducing

cmake -S bench/cpp -B bench/cpp/build -DCMAKE_BUILD_TYPE=Release && cmake --build bench/cpp/build -j
cargo build --release --manifest-path bench/rust/Cargo.toml

bench/run.sh --duration 30 --reps 3      # takes the simulator lock, one container per cell

bench/README.md documents the harness, the --hardware mode and its guards, and bench/fer-capture/README.md the wire-capture tooling. Raw per-run JSON of the simulator runs lives under bench/results/.