Blog

Débit par cycle CPU : le chiffre qui survit au contact d'un routeur

Un chiffre de laboratoire et un chiffre matériel concordent presque jamais. Nous avons mesuré les deux, conservé les intervalles, révélé le piège qui a produit un résultat erroné, et dit clairement où nous perdons.

Publié le 6 septembre 2026

Sur deux routeurs physiques, en faisant transiter du trafic réel sur un vrai réseau local gigabit, le cœur de notre tunnel a poussé 122 Mbit/s là où l'implémentation de tunnel du noyau propre au routeur a poussé 66.8 Mbit/s — sur le même émetteur, avec moins de CPU par Mbit. Aucun des deux chiffres ne raconte l'histoire à lui seul : les deux volets ont dû utiliser des générateurs de trafic différents, car aucun générateur unique ne fonctionne des deux côtés de cette comparaison. Le ratio à discuter est donc chaque volet face à son propre plafond sans tunnel, qui figure dans le tableau ci-dessous et nous favorise davantage, pas moins. Un essai de laboratoire à quatre VM sur un ordinateur portable annonce quelque chose de plus rapide en valeur absolue et, sur un axe, quelque chose de moins flatteur. Nous accordons plus de confiance au chiffre du routeur, car un routeur est le seul des deux que le trafic d'un client traverse réellement.

C'est le résultat qui mérite d'ouvrir cet article, et aussi la raison pour laquelle il comporte deux sections de résultats plutôt qu'une seule : le laboratoire et le matériel ne s'accordent pas sur l'axe qui compte, et masquer ce désaccord coûterait plus de crédibilité qu'aucun des deux chiffres n'en gagne.

À lire avant les chiffres. Notre plan de données utilise un seul thread par défaut. Le comparateur que nous avons fait tourner à côté en laboratoire en utilise quatre par défaut. Sans égalisation, ce seul réglage fait varier son débit d'environ 789 à 1427 Mbit/s — un écart de réglages par défaut, pas de chemins de données — et cela a déjà produit ici un chiffre publié erroné. Chaque chiffre de laboratoire ci-dessous fixe les deux cœurs Rust à un seul thread. Les implémentations Go et noyau de ce tableau ont tourné avec leurs propres réglages par défaut, qui en utilisent plus d'un — ce qui, si quoi que ce soit, les avantage face à notre thread unique. L'écart entre réglages par défaut tels que livrés est montré séparément, une fois, pour ne pas non plus le cacher par omission.

Ce qui a été mesuré, et sur quoi

Deux environnements indépendants, car un benchmark n'est honnête que dans l'environnement où il a tourné — un chiffre iperf3 et un chiffre bandwidth-test ne sont pas un seul chiffre portant deux noms, et une VM partageant les cœurs d'un ordinateur portable n'est pas un routeur.

Matériel — deux routeurs MikroTik, un réseau local gigabit, 2026-09-01. RB3011UiAS (ARMv7, 2×1400 MHz) comme émetteur, cAP ax (ARM64, 4×864 MHz) comme récepteur, RouterOS 7.24.1 sur les deux. Notre cœur a tourné en tant que fv, dans un conteneur RouterOS, en espace utilisateur, face à l'implémentation du protocole WireGuard intégrée au noyau du routeur — le seul comparateur réellement équivalent disponible sur ce matériel, puisque c'est la seule alternative livrée avec RouterOS lui-même. Deux générateurs, car aucun ne traverse proprement les implémentations : dd | nc pour notre exécution en conteneur, le bandwidth-test propre à RouterOS pour l'exécution noyau — chacun porte son propre plafond sans tunnel comme témoin, pour qu'un lecteur puisse vérifier le plafond avant de faire confiance au ratio.

Laboratoire — quatre invités Debian aarch64 sous QEMU/HVF sur un hôte Apple Silicon, 2026-08-31. Mêmes paires de clés, adresses, ports, MTU et mise en service des interfaces pour chaque binaire testé : notre cœur, boringtun 0.7 (le cœur en espace utilisateur dont le nôtre est dérivé), wireguard-go (l'implémentation de référence du protocole WireGuard, en espace utilisateur), et le module noyau Linux du protocole WireGuard lui-même. Sessions iperf3 de 15 secondes, trois échantillons chacune, les deux cœurs Rust fixés à un seul thread de plan de données, pour la raison donnée dans l'encadré ci-dessus.

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

L'exécution sur routeur n'est pas encore une cible task — c'est fv lancé à la main dans un conteneur RouterOS sur chaque boîtier, face au serveur du protocole WireGuard intégré à RouterOS. La topologie complète et chaque ligne brute se trouvent dans notre propre docs/benchmarks/results.csv.

Le chiffre qui compte : sur le routeur

Mesurenotre cœur (espace utilisateur)tunnel noyau propre au routeur
débit122 Mbit/s66.8 Mbit/s
CPU, émetteur65% (63–67)74% (70–79)
CPU, récepteur18% (10–22)20% (18–21)
CPU par Mbit0.53 %/Mbit1.10 %/Mbit
RTT1.22 ms0.92 ms
mémoire résidente (émetteur / récepteur)3.0 / 3.1 MBen noyau, non séparable
générateurdd | ncbandwidth-test
plafond sans tunnel de ce générateur187 Mbit/s220.5 Mbit/s
retenu face à son propre plafond65%30%
our core122 Mbit/srouter's kernel tunnel66.8 Mbit/s

Longueur de barre ∝ Mbit/s, axe 0–140. Même émetteur RB3011 dans les deux exécutions ; c'est le plafond CPU tout du long, entre 65–79 % pendant que le cAP ax récepteur reste proche de 20 % au repos.

Deux angles indépendants concordent, ce qui rend le résultat crédible plutôt que trié sur le volet : le débit absolu favorise notre cœur 1.8×, et le débit retenu face au plafond incontesté propre à chaque générateur le favorise 2.2×. Ce que le tableau ne dit pas : les deux générateurs ne sont pas le même outil, ce qui explique pourquoi les deux plafonds sans tunnel sont imprimés plutôt qu'un seul ratio en gros titre, et les deux chiffres bougeraient si l'émetteur recevait du vrai trafic réseau au lieu de le générer localement. Contrairement au tableau de laboratoire ci-dessous, ce sont des exécutions uniques : les pourcentages entre parenthèses sont la plage dans laquelle le CPU a varié pendant cette exécution, pas une dispersion sur plusieurs essais répétés.

Le laboratoire : trois cœurs en espace utilisateur, un noyau, threads égalisés

cœurtypeTCP Mbit/s (plage, n=3)mémoire de pointelatence p99
le nôtre, 1 threadespace utilisateur, Rust1535 (1506–1554)3.5 MB0.94 ms
boringtun, 1 threadespace utilisateur, Rust1427 (1346–1479)5.3 MB1.15 ms
wireguard-go, par défautespace utilisateur, Go1228 (1221–1239)34.8 MB1.39 ms
WireGuard noyau, par défautnoyau, C1376 (1366–1386)— (noyau, non séparable)0.96 ms

Tel que livré, sans égalisation : notre cœur utilise 1 thread par défaut et a tourné à 1535 Mbit/s ; boringtun utilise 4 threads par défaut et a tourné à ~789 Mbit/s avec ce réglage. Ne rapporter que le chiffre par-défaut-contre-par-défaut répéterait la même erreur, dans le sens flatteur. Les deux chiffres sont réels ; un seul d'entre eux isole le chemin de données du nombre de threads.

Le coût CPU est ce qui justifie la place du laboratoire dans cet article — c'est le seul axe que le tableau du routeur ne peut pas montrer, car un comparateur intégré au noyau du routeur ne fournit aucune décomposition du CPU à comparer.

usersysour core0.90 s/Gbit totaluser 0.347 · sys 0.549 s/Gbitboringtun1.08 s/Gbit totaluser 0.290 · sys 0.787 s/Gbitwireguard-go2.37 s/Gbit totaluser 0.876 · sys 1.490 s/Gbit

Plus bas est meilleur. Axe 0–2.5 s/Gbit, linéaire. Les deux segments s'empilent de gauche à droite ; la coupure est l'endroit où la perte de la section suivante apparaît.

our core3.5 MBboringtun5.3 MBwireguard-go34.8 MB

Échelle logarithmique, 1–50 MB, précisée car ce n'est pas le réglage par défaut honnête : sur un axe linéaire, les 34.8 MB de wireguard-go écraseraient les deux autres barres en simples traits.

Là où nous n'avons pas gagné

Deux endroits, tous deux expliqués plutôt qu'enfouis dans le CSV :

Le CPU en mode utilisateur, en laboratoire. 0.347 s/Gbit contre 0.290 pour le comparateur Rust moins coûteux — la seule colonne de ce tableau où il nous devance. Notre lecture est que c'est un coût acheté, pas gaspillé : notre cœur effectue le déchargement du calcul de somme de contrôle TUN dans les deux sens, ce qui déplace du travail hors du noyau vers le CPU en espace utilisateur, comptabilisé comme temps utilisateur — et la colonne CPU système, où ce compromis est remboursé, est nettement en avance (0.549 contre 0.787). Le CPU total gagne quand même. La répartition utilisateur/système ci-dessus est montrée précisément pour que ce compromis soit visible plutôt qu'affirmé.

Le temps d'aller-retour, sur le routeur. 1.22 ms contre 0.92 ms pour le tunnel noyau — le seul axe matériel où le noyau l'emporte nettement, pour la raison ordinaire que notre chemin coûte un saut veth de conteneur et un tour d'ordonnancement en espace utilisateur qu'un chemin dans le noyau ne paie pas. Fait intéressant, le laboratoire montre l'ordre inverse sur la latence p99 (0.94 ms contre 0.96 ms pour le noyau) — nous n'en tirons rien de plus que « environnements différents, goulots d'étranglement différents », et le disons ainsi plutôt que de choisir le chiffre le plus flatteur.

Ce qui est récent et ce qui est hérité

Chaque chiffre ci-dessus porte une date, et ces dates comptent : le tableau du routeur est du 2026-09-01, celui du laboratoire du 2026-08-31. Depuis, une montée de version majeure coordonnée a fait évoluer les crates de cryptographie sous-jacentes que notre plan de données appelle à chaque paquet — chacha20poly1305, blake2, curve25519-dalek — et nous n'avons pas encore relancé la comparaison complète en laboratoire ni sur routeur avec cet arbre. Les chiffres ci-dessus sont réels et ont été mesurés avec soin, mais ils ont six jours au moment de la publication, sur un arbre antérieur à cette montée de version. Nous les publions en le précisant plutôt que silencieusement, et nous daterons une nouvelle exécution quand nous en aurons une. Le chiffre du routeur n'est pas non plus rattaché à un commit précis dans nos propres archives — lisez « 2026-09-01 » comme « début septembre », pas comme lié à un changement d'optimisation particulier.

Ce que nous pouvions relancer aujourd'hui, sur cette machine, sans routeur ni laboratoire de VM, c'est la partie du chemin que cette montée de version cryptographique a touchée le plus directement : le coût d'encapsulation et de décapsulation, isolé de toute préoccupation de socket, de TUN ou de threads évoquée ci-dessus.

opération, taille de paquetnotre cœurboringtun 0.7.1
encapsulation, 64 B219.2 ns218.7 ns
encapsulation, 512 B436.4 ns442.5 ns
encapsulation, 1420 B938.1 ns968.3 ns
décapsulation, 64 B238.8 ns226.7 ns
décapsulation, 512 B562.1 ns539.2 ns
décapsulation, 1420 B1099.5 ns1069.4 ns

Nouvelle exécution le 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 échantillons par point. Aucun socket, aucun TUN, aucun thread — une négociation terminée, puis encapsulation/décapsulation en boucle. Les résultats sont mitigés selon le sens et la taille de paquet, tous à quelques pourcents près dans un sens ou l'autre, ce qui est exactement ce à quoi on s'attendrait : notre cœur a dérivé ce chemin de son amont, et les deux reposent désormais sur la même génération de primitives RustCrypto. Ce n'est pas une revendication de débit — cela dit que le chemin cryptographique dérivé coûte à peu près ce que coûte celui de l'amont, aux fluctuations près, ce qui est précisément ce qui rend crédibles les victoires du laboratoire et du routeur ci-dessus : elles viennent d'ailleurs dans la pile (regroupement des appels système, déchargement, threads), pas d'un chiffrement plus rapide.

Reproduire

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

L'exécution sur routeur est manuelle aujourd'hui : fv dans un conteneur RouterOS sur chaque boîtier, face au serveur du protocole WireGuard propre à RouterOS.

Pour être direct sur ce que cela vous apporte réellement : le harnais et le results.csv brut derrière chaque tableau ici vivent dans notre arbre, qui n'est pas public aujourd'hui. Lisez les commandes ci-dessus comme un énoncé exact de ce que nous avons exécuté, pas comme quelque chose que vous pouvez cloner et répéter. L'affirmation la moins dépendante de nous est aussi celle qui mérite le plus d'être vérifiée — boringtun et wireguard-go sont publics, iperf3 est public, et le piège du nombre de threads en tête de cet article (un seul réglage faisant varier un comparateur entre environ 789 et 1427 Mbit/s sur une seule machine) est reproductible par quiconque dispose de ces trois-là et d'aucune ligne de notre code dans la boucle. Si cela se confirme pour vous, ce sont les lignes égalisées ci-dessus qu'il faut discuter. Si ce n'est pas le cas, nous voulons en entendre parler.

Tous les articles

Français
Posez-nous vos questions