Présentation
benchfmt.rs est un exécuteur de micro‑benchmarks écrit en Rust, sans dépendance externe. Il génère directement le format benchfmt utilisé par l’outil benchstat de Go, ce qui permet d’appliquer les mêmes pipelines d’analyse aux projets Rust. Le projet ajoute trois fonctionnalités absentes des outils standards Rust : projections /key=value, suivi du nombre d’octets alloués (B/op) et du nombre d’objets (allocs/op) par opération, ainsi qu’une intégration du profileur pprof.
Fonctionnement de benchfmt.rs
Le runner s’appuie sur un compteur d’allocation de tas thread‑local. À chaque itération du benchmark, le compteur est remis à zéro, le code testé s’exécute, puis le nombre d’octets et d’objets alloués est relevé. Ces métriques sont injectées dans la sortie benchfmt sous la forme /key=value, exactement comme le fait go test -bench. Le même binaire inclut également deux convertisseurs : un qui transforme les résultats de Criterion et un qui convertit les sorties du crate libtest en format Go, garantissant une compatibilité totale avec benchstat sans modification du code source testé.
func BenchmarkDecodeVarints(b *testing.B) {
raw, _ := os.ReadFile("varints_2000.bin")
dst := make([]uint64, 2000)
b.Run("samples=2000/lang=go", func(b *testing.B) {
b.SetBytes(int64(len(raw)))
b.ReportAllocs()
for b.Loop() {
n, err := DecodeVarints(raw, dst)
if err != nil || n != 2000 {
b.Fatalf("unexpected decode result")
}
}
})
}
Le même schéma de sous‑benchmarks (/samples=2000/lang=go) est reproduit en Rust grâce à benchfmt.rs, ce qui rend possible la pivotation sur les dimensions samples ou lang dans benchstat.
Comparaison des performances Go vs Rust
Deux charges de travail ont été évaluées : le décodage de varints LEB128 et le décodage du protocole Prometheus Remote Write 2.0. Le fichier de varints (varints_2000.bin) pèse 4 798 octets et contient 2 000 valeurs uint64. Le décodage s’effectue sans allocation supplémentaire grâce à un tableau pré‑alloué. Pour Remote Write 2.0, les fixtures de 200 et 2 000 séries occupent respectivement 244,9 KiB (décompressé) et 57,0 KiB (compressé). Les benchmarks ont été exécutés pendant six « epochs », chacune de deux secondes, sur une station de travail Intel Xeon 2.20 GHz (24 cœurs) avec go test (Go 1.26.8) et cargo bench (Rust 1.99.1), en limitant l’usage CPU à deux cœurs (-cpu 2).
Les résultats affichés par benchstat montrent que, pour le décodage de varints, Rust dépasse Go de plusieurs dizaines de pourcents en termes de nanosecondes par opération, tout en conservant zéro allocation (B/op = 0, allocs/op = 0). Pour Remote Write 2.0, la version Rust, qui utilise le crate prost et le décompresseur snap, réalise également des gains de performance notables, notamment sur les gros lots de 2 000 séries où la surcharge de désérialisation devient critique. Les mesures d’allocation confirment que Rust maintient une empreinte mémoire comparable voire inférieure à celle de Go, grâce à une gestion plus fine du cycle de vie des buffers.
Analyse des limites et perspectives
Le principal point faible de benchfmt.rs réside dans son adoption encore restreinte : les projets Rust doivent intégrer explicitement le crate et adapter leurs suites de tests pour exploiter les flags -bench compatibles Go. De plus, le compteur d’allocation thread‑local ne capture pas les allocations indirectes réalisées par des bibliothèques C appelées via FFI, ce qui peut biaiser les métriques dans des scénarios hybrides. Enfin, la comparaison présentée repose sur des charges de travail très spécifiques (varint et Remote Write) ; d’autres domaines (graphes, bases de données) pourraient révéler des écarts différents.
Malgré ces réserves, benchfmt.rs fournit une passerelle concrète entre les écosystèmes de benchmarking Go et Rust, facilitant les études A/B et la standardisation des métriques de performance. L’ajout futur d’un support natif pour les profils perf ou d’une API HTTP pour exporter les résultats pourrait élargir son utilisation dans les pipelines CI/CD.