Blog

Durchsatz pro CPU-Takt: die Kennzahl, die den Kontakt mit einem Router übersteht

Eine Laborzahl und eine Hardwarezahl stimmen fast nie überein. Wir haben beide gemessen, die Bandbreiten beibehalten, die Falle offengelegt, die einmal ein falsches Ergebnis erzeugt hat, und offen gesagt, wo wir verloren haben.

Veröffentlicht am 6. September 2026

Auf zwei physischen Routern, mit echtem Datenverkehr über ein echtes Gigabit-LAN, erreichte unser Tunnel-Kern 122 Mbit/s, wo die eigene Kernel-Tunnel-Implementierung des Routers 66.8 Mbit/s erreichte — auf demselben Sender, bei weniger CPU pro Mbit. Keine der beiden Zahlen erzählt für sich allein die ganze Geschichte: Die beiden Testarme mussten unterschiedliche Traffic-Generatoren verwenden, weil kein einzelner Generator auf beiden Seiten dieses Vergleichs läuft. Das Verhältnis, über das es sich zu streiten lohnt, ist also jeder Arm gegen seine eigene Ohne-Tunnel-Obergrenze, die in der Tabelle unten steht und uns um mehr, nicht weniger, begünstigt. Ein Vier-VM-Labortest auf einem Laptop liefert absolut gesehen eine höhere Zahl und auf einer Achse etwas weniger Schmeichelhaftes. Wir vertrauen der Router-Zahl mehr, weil ein Router von beiden der Einzige ist, den der Datenverkehr eines Kunden tatsächlich durchquert.

Das ist der Befund, mit dem es sich zu beginnen lohnt, und auch der Grund, warum dieser Beitrag zwei Ergebnisabschnitte statt einem hat: Labor und Hardware sind sich uneinig, welche Achse zählt, und dieser Uneinigkeit einen Anstrich zu geben, würde mehr Glaubwürdigkeit kosten, als eine der beiden Zahlen einbringt.

Lesen Sie dies vor den Zahlen. Unsere Datenebene läuft standardmäßig mit einem Thread. Der Vergleichswert, den wir im Labor daneben laufen ließen, läuft standardmäßig mit vieren. Ungleich gelassen, verschiebt allein dieses Flag seinen Durchsatz von ~789 auf 1427 Mbit/s — eine Lücke in den Standardeinstellungen, nicht in den Datenpfaden — und sie hat hier bereits einmal eine falsche veröffentlichte Zahl hervorgebracht. Jede Laborzahl unten fixiert beide Rust-Kerne auf einen Thread. Die Go- und die Kernel-Implementierung in jener Tabelle liefen mit ihren eigenen Standardeinstellungen, die mehr als einen Thread nutzen — wenn überhaupt, begünstigt das sie gegenüber unserem einzelnen Thread. Die Lücke bei den Standardeinstellungen, so wie ausgeliefert, wird separat, einmal, gezeigt, damit sie auch nicht durch Auslassung verborgen bleibt.

Was gemessen wurde, und womit

Zwei unabhängige Umgebungen, denn ein Benchmark ist nur innerhalb der Umgebung ehrlich, in der er lief — eine iperf3-Zahl und eine bandwidth-test-Zahl sind nicht eine Zahl mit zwei Namen, und eine VM, die sich die Kerne eines Laptops teilt, ist kein Router.

Hardware — zwei MikroTik-Router, ein Gigabit-LAN, 2026-09-01. RB3011UiAS (ARMv7, 2×1400 MHz) als Sender, cAP ax (ARM64, 4×864 MHz) als Empfänger, RouterOS 7.24.1 auf beiden. Unser Kern lief als fv, in einem RouterOS-Container, im Userspace, gegen die eigene In-Kernel-WireGuard-Protokoll-Implementierung des Routers — der einzige direkt vergleichbare Wert, der auf dieser Hardware verfügbar ist, da es die einzige Alternative ist, die in RouterOS selbst mitgeliefert wird. Zwei Generatoren, weil keiner die Implementierungen sauber übergreift: dd | nc für unseren Container-Lauf, RouterOS' eigenes bandwidth-test für den Kernel-Lauf — jeder trägt seine eigene Ohne-Tunnel-Obergrenze als Kontrolle mit sich, sodass eine Leserin die Obergrenze prüfen kann, bevor sie dem Verhältnis vertraut.

Labor — vier aarch64-Debian-Gäste unter QEMU/HVF auf einem Host mit Apple-Silicon, 2026-08-31. Dieselben Schlüsselpaare, Adressen, Ports, MTU und derselbe Interface-Aufbau für jede getestete Binärdatei: unser Kern, boringtun 0.7 (der Userspace-Kern, von dem unserer abgeleitet wurde), wireguard-go (die Referenzimplementierung des WireGuard-Protokolls, Userspace) und das WireGuard-Protokoll-Modul des Linux-Kernels selbst. 15-Sekunden-Läufe mit iperf3, je drei Stichproben, beide Rust-Kerne aus dem oben genannten Grund auf einen Datenebenen-Thread festgelegt.

task test:impls    # lab: ours vs boringtun vs wireguard-go vs the kernel
task test:all      # lab: single-hop, multi-hop, and the comparison

Der Router-Lauf ist noch kein task-Ziel — es ist fv, von Hand in einem RouterOS-Container auf jeder Box gestartet, gegen den eingebauten WireGuard-Protokoll-Server von RouterOS. Die vollständige Topologie und jede Rohzeile stehen in unserer eigenen docs/benchmarks/results.csv.

Die Zahl, auf die es ankommt: auf dem Router

Messwertunser Kern (Userspace)eigener Kernel-Tunnel des Routers
Durchsatz122 Mbit/s66.8 Mbit/s
CPU, Sender65% (63–67)74% (70–79)
CPU, Empfänger18% (10–22)20% (18–21)
CPU pro Mbit0.53 %/Mbit1.10 %/Mbit
RTT1.22 ms0.92 ms
Resident-Speicher (Sender / Empfänger)3.0 / 3.1 MBim Kernel, nicht trennbar
Generatordd | ncbandwidth-test
Ohne-Tunnel-Obergrenze dieses Generators187 Mbit/s220.5 Mbit/s
gegenüber der eigenen Obergrenze gehalten65%30%
unser Kern122 Mbit/sKernel-Tunnel des Routers66.8 Mbit/s

Balkenlänge ∝ Mbit/s, Achse 0–140. Derselbe RB3011-Sender in beiden Läufen; er ist durchgehend die CPU-Obergrenze, bei 65–79 %, während der empfangende cAP ax nahe 20 % im Leerlauf bleibt.

Zwei unabhängige Betrachtungsweisen stimmen überein, was das Ergebnis glaubwürdig macht statt herausgepickt: Der absolute Durchsatz begünstigt unseren Kern um das 1.8-Fache, und der gegenüber der unangefochtenen eigenen Obergrenze jedes Generators gehaltene Durchsatz begünstigt ihn um das 2.2-Fache. Was die Tabelle nicht sagt: Die beiden Generatoren sind nicht dasselbe Werkzeug, weshalb beide Ohne-Tunnel-Obergrenzen abgedruckt sind statt eines einzigen Kennzahlenverhältnisses, und beide Werte würden sich verschieben, wenn der Sender echten Netzwerkverkehr empfinge statt ihn lokal zu erzeugen. Anders als die Labortabelle unten sind das Einzelläufe: Die Prozentangaben in Klammern sind die Spanne, über die die CPU während dieses Laufs schwankte, keine Streuung über wiederholte Versuche.

Das Labor: drei Userspace-Kerne, ein Kernel, angeglichene Threads

KernArtTCP Mbit/s (Spanne, n=3)Spitzenspeicherp99-Latenz
unser Kern, 1 ThreadUserspace, Rust1535 (1506–1554)3.5 MB0.94 ms
boringtun, 1 ThreadUserspace, Rust1427 (1346–1479)5.3 MB1.15 ms
wireguard-go, StandardUserspace, Go1228 (1221–1239)34.8 MB1.39 ms
Kernel-WireGuard, StandardKernel, C1376 (1366–1386)— (Kernel, nicht trennbar)0.96 ms

So wie ausgeliefert, ungeglichen: Unser Kern läuft standardmäßig mit 1 Thread und erreichte 1535 Mbit/s; boringtun läuft standardmäßig mit 4 Threads und erreichte bei dieser Einstellung ~789 Mbit/s. Nur die Standard-gegen-Standard-Zahl zu berichten, würde denselben Fehler in die schmeichelhafte Richtung wiederholen. Beide Zahlen sind real; nur eine von ihnen isoliert den Datenpfad von der Thread-Anzahl.

Die CPU-Kosten sind der Punkt, an dem sich das Labor seinen Platz in diesem Beitrag verdient — es ist die eine Achse, die die Router-Tabelle nicht zeigen kann, weil der In-Kernel-Vergleichswert eines Routers keine CPU-Aufschlüsselung zum Vergleich liefert.

UserSysunser Kern0.90 s/Gbit insgesamtUser 0.347 · Sys 0.549 s/Gbitboringtun1.08 s/Gbit insgesamtUser 0.290 · Sys 0.787 s/Gbitwireguard-go2.37 s/Gbit insgesamtUser 0.876 · Sys 1.490 s/Gbit

Niedriger ist besser. Achse 0–2.5 s/Gbit, linear. Beide Segmente stapeln sich von links nach rechts; die Trennung zeigt, wo der Verlust im nächsten Abschnitt sichtbar wird.

unser Kern3.5 MBboringtun5.3 MBwireguard-go34.8 MB

Logarithmische Skala, 1–50 MB, so angegeben, weil sie nicht die ehrliche Standardeinstellung ist: Auf einer linearen Achse würden die 34.8 MB von wireguard-go die anderen beiden Balken zu Haarlinien zusammendrücken.

Wo wir nicht gewonnen haben

Zwei Stellen, beide erklärt statt in der CSV vergraben:

User-Mode-CPU, im Labor. 0.347 s/Gbit gegen die 0.290 des günstigeren Rust-Vergleichswerts — die eine Spalte in jener Tabelle, in der er vor uns liegt. Unsere Lesart ist, dass das erkauft ist, nicht verschwendet: Unser Kern führt das TUN-Prüfsummen-Offloading in beide Richtungen aus, was Arbeit aus dem Kernel heraus- und in Userspace-CPU hineinverlagert, die als User-Zeit verbucht wird — und die System-CPU-Spalte, in der sich dieser Tausch auszahlt, liegt deutlich vorn (0.549 gegenüber 0.787). Die CPU insgesamt gewinnt trotzdem. Die User/Sys-Aufteilung oben wird gerade deshalb gezeigt, damit dieser Tausch sichtbar ist statt nur behauptet.

Umlaufzeit, auf dem Router. 1.22 ms gegen die 0.92 ms des Kernel-Tunnels — die eine Hardware-Achse, auf der In-Kernel klar gewinnt, aus dem gewöhnlichen Grund, dass unser Pfad einen veth-Hop des Containers und eine Userspace-Scheduling-Runde kostet, die ein In-Kernel-Pfad nicht bezahlt. Interessanterweise zeigt das Labor bei der p99-Latenz die umgekehrte Reihenfolge (0.94 ms gegen 0.96 ms beim Kernel) — wir lesen darin nichts hinein außer „unterschiedliche Umgebungen, unterschiedliche Engpässe", und sagen das auch so, statt uns die Zahl herauszupicken, die mehr schmeichelt.

Was frisch ist und was übernommen wurde

Jede Zahl oben trägt ein Datum, und die Daten sind wichtig: Die Router-Tabelle ist vom 2026-09-01, die Labortabelle vom 2026-08-31. Seitdem hat ein koordinierter Hauptversions-Sprung die zugrunde liegenden Krypto-Crates verschoben, die unsere Datenebene bei jedem Paket aufruft — chacha20poly1305, blake2, curve25519-dalek — und wir haben den vollständigen Labor- oder Router-Vergleich gegen diesen Stand nicht erneut durchgeführt. Die Zahlen oben sind real und wurden sorgfältig gemessen, aber sie sind zum Zeitpunkt der Veröffentlichung sechs Tage alt, auf einem Stand, der vor diesem Sprung liegt. Wir veröffentlichen sie mit diesem Hinweis statt stillschweigend, und wir werden einen erneuten Lauf datieren, sobald wir einen haben. Die Router-Zahl ist in unseren eigenen Aufzeichnungen auch nicht auf einen bestimmten Commit festgelegt — lesen Sie „2026-09-01" als „Anfang September", nicht als an eine einzelne Optimierungsänderung gebunden.

Was wir heute, auf dieser Maschine, ohne Router oder VM-Labor erneut ausführen könnten, ist der Teil des Pfads, den der Krypto-Sprung am direktesten betraf: die Kosten für Ein- und Auspacken (Encapsulate/Decapsulate), losgelöst von jedem Socket-, TUN- und Threading-Aspekt oben.

Operation, Paketgrößeunser Kernboringtun 0.7.1
Encapsulate, 64 B219.2 ns218.7 ns
Encapsulate, 512 B436.4 ns442.5 ns
Encapsulate, 1420 B938.1 ns968.3 ns
Decapsulate, 64 B238.8 ns226.7 ns
Decapsulate, 512 B562.1 ns539.2 ns
Decapsulate, 1420 B1099.5 ns1069.4 ns

Erneuter Lauf am 2026-09-05, Apple M3 Max, cargo bench -p forestvpn-quantum --bench datapath_benches -- --measurement-time 3 --warm-up-time 1, criterion 0.8.2, 100 Stichproben pro Punkt. Kein Socket, kein TUN, keine Threads — ein abgeschlossener Handshake, dann Encapsulate/Decapsulate in einer Schleife. Die Ergebnisse sind je nach Richtung und Paketgröße gemischt, alle innerhalb weniger Prozent in die eine oder andere Richtung, genau wie zu erwarten war: Unser Kern hat diesen Pfad von seinem Upstream abgezweigt, und beide sitzen jetzt auf derselben Generation von RustCrypto-Primitiven. Das ist keine Durchsatzaussage — sie besagt, dass der abgezweigte Krypto-Pfad selbst innerhalb des Rauschens etwa das kostet, was der Upstream kostet, und genau das macht die Labor- und Router-Erfolge oben vertrauenswürdig: Sie kommen aus einem anderen Teil des Stacks (Syscall-Batching, Offloading, Threading), nicht aus einer schnelleren Chiffre.

Reproduzieren

task test:impls          # lab: ours vs boringtun vs wireguard-go vs the kernel
task test:all            # lab: single-hop, multi-hop, and the multihop cost table
cargo bench -p forestvpn-quantum --bench datapath_benches -- --measurement-time 3 --warm-up-time 1

Der Router-Lauf ist heute manuell: fv in einem RouterOS-Container auf jeder Box, gegen den eigenen WireGuard-Protokoll-Server von RouterOS.

Um ehrlich zu sagen, was Ihnen das bringt: Der Testrahmen und die rohe results.csv hinter jeder Tabelle hier liegen in unserem eigenen Repository, das heute nicht öffentlich ist. Lesen Sie die Befehle oben als exakte Aussage darüber, was wir ausgeführt haben, nicht als etwas, das Sie klonen und wiederholen können. Die Behauptung, die am wenigsten von uns abhängt, ist auch die, die es am meisten zu prüfen lohnt — boringtun und wireguard-go sind öffentlich, iperf3 ist öffentlich, und die Thread-Anzahl-Falle am Anfang dieses Beitrags (ein Flag, das einen Vergleichswert auf einem einzelnen Host zwischen etwa 789 und 1427 Mbit/s verschiebt) lässt sich von jedem mit diesen dreien und ohne unseren Code im Spiel reproduzieren. Wenn es sich für Sie bestätigt, sind die angeglichenen Zeilen oben die, über die es sich zu streiten lohnt. Wenn nicht, wollen wir davon hören.

Alle Beiträge

Deutsch
Fragen Sie uns alles