🔝 Retour au Sommaire
Niveau : Avancé
Prérequis : Sections 24.6.1 à 24.6.3, chapitre 35 (Benchmarking avec Google Benchmark)
Outils : Google Benchmark, perf (chapitre 31), Valgrind/Massif (chapitre 30)
Environnement de test : Ubuntu 24.04 LTS, AMD Ryzen 9 7950X (Zen 4), 64 Go DDR5, GCC 15.1 (-O2 -std=c++23), Clang 20.1 (-O2 -std=c++23)
Les chiffres présentés dans les sections précédentes donnent des ordres de grandeur. Mais les performances d'un moteur regex dépendent fortement de trois facteurs interdépendants : la complexité du pattern, la longueur et la structure du texte d'entrée, et le type d'opération (match total, recherche, itération, remplacement). Un moteur peut dominer sur un profil de charge et être dépassé sur un autre.
Cette section fournit une méthodologie de benchmarking reproductible, des résultats sur plusieurs profils représentatifs, et un guide de choix consolidé qui intègre les dimensions non couvertes par les seuls chiffres de performance.
Tous les benchmarks utilisent Google Benchmark (chapitre 35) avec les précautions suivantes pour garantir des résultats fiables :
- Isolation CPU : les benchmarks sont épinglés sur un cœur dédié via
tasksetpour éliminer les migrations de threads et la variance inter-cœur. - Fréquence fixe : le gouverneur CPU est réglé sur
performancepour désactiver le scaling dynamique. - Warm-up : chaque benchmark inclut une phase de warm-up de 500 ms avant la mesure pour stabiliser les caches.
- Itérations : Google Benchmark détermine automatiquement le nombre d'itérations pour atteindre une précision statistique cible.
- Pattern pré-compilé : pour les moteurs runtime (RE2, PCRE2,
std::regex), le pattern est compilé avant la boucle de mesure. On mesure le coût du matching, pas de la compilation.
#include <benchmark/benchmark.h>
#include <regex>
#include <re2/re2.h>
#include <ctre.hpp>
// Données partagées
static const std::string log_line =
"[2026-03-21T14:32:07.482Z] [ERROR] [auth-service] "
"Connection refused to 192.168.1.42:5432 after 3 retries (timeout=30s)";
// ---------- std::regex ----------
static const std::regex std_pattern(
R"((\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})\.\d+Z\]\s+\[(\w+)\])"
);
static void BM_StdRegex(benchmark::State& state) {
std::smatch match;
for (auto _ : state) {
benchmark::DoNotOptimize(std::regex_search(log_line, match, std_pattern));
}
}
BENCHMARK(BM_StdRegex);
// ---------- RE2 ----------
static const RE2 re2_pattern(
R"((\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})\.\d+Z\]\s+\[(\w+)\])"
);
static void BM_RE2(benchmark::State& state) {
std::string date, time, level;
for (auto _ : state) {
benchmark::DoNotOptimize(
RE2::PartialMatch(log_line, re2_pattern, &date, &time, &level)
);
}
}
BENCHMARK(BM_RE2);
// ---------- CTRE ----------
static void BM_CTRE(benchmark::State& state) {
for (auto _ : state) {
benchmark::DoNotOptimize(
ctre::search<
R"((\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})\.\d+Z\]\s+\[(\w+)\])"
>(log_line)
);
}
}
BENCHMARK(BM_CTRE);
BENCHMARK_MAIN();Le CMakeLists.txt correspondant :
find_package(benchmark REQUIRED)
find_package(re2 REQUIRED)
add_executable(regex_bench regex_bench.cpp)
target_link_libraries(regex_bench
PRIVATE benchmark::benchmark re2::re2 ctre::ctre
)
target_compile_features(regex_bench PRIVATE cxx_std_23)Pattern : (\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})
Entrée : ligne de log de 120 caractères (correspondance au début)
| Moteur | Temps/op | Ratio vs CTRE | Allocations/op |
|---|---|---|---|
std::regex |
2 750 ns | ×92 | 7 |
| RE2 | 145 ns | ×4.8 | 0 |
| PCRE2 (JIT) | 110 ns | ×3.7 | 0 |
| PCRE2 (interprété) | 340 ns | ×11 | 0 |
| CTRE | 30 ns | ×1 | 0 |
CTRE domine largement grâce à l'absence totale de coût d'interprétation. RE2 et PCRE2 JIT sont dans le même ordre de grandeur. std::regex est près de 100 fois plus lent.
Pattern : \d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(:\d{1,5})?
Entrée : chaîne courte de 21 caractères ("192.168.1.42:8080")
| Moteur | Temps/op | Ratio vs CTRE | Allocations/op |
|---|---|---|---|
std::regex |
1 850 ns | ×74 | 5 |
| RE2 | 95 ns | ×3.8 | 0 |
| PCRE2 (JIT) | 55 ns | ×2.2 | 0 |
| CTRE | 25 ns | ×1 | 0 |
Sur les entrées courtes, PCRE2 JIT se rapproche de CTRE car le coût fixe par appel représente une plus grande proportion du temps total pour RE2 (construction du contexte DFA).
Pattern : (\w+)=(\w+)
Entrée : chaîne de requête HTTP de 350 caractères contenant 12 paires clé=valeur
| Moteur | Temps/op (12 matches) | Ratio vs CTRE | Allocations/op |
|---|---|---|---|
std::regex (sregex_iterator) |
38 400 ns | ×107 | 84 |
| RE2 (FindAndConsume) | 1 820 ns | ×5.1 | 0 |
| PCRE2 JIT (boucle manuelle) | 1 450 ns | ×4.0 | 0 |
| CTRE (ctre::range) | 360 ns | ×1 | 0 |
C'est le profil où std::regex souffre le plus. Chaque itération avec sregex_iterator provoque des allocations pour stocker les résultats intermédiaires. Sur 12 correspondances, cela cumule 84 allocations contre zéro pour les trois alternatives.
Pattern : (GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+(/[^\s]*)\s+HTTP/(\d\.\d)
Entrée : ligne de log d'accès HTTP de 180 caractères
| Moteur | Temps/op | Ratio vs CTRE | Allocations/op |
|---|---|---|---|
std::regex |
4 200 ns | ×84 | 9 |
| RE2 | 210 ns | ×4.2 | 0 |
| PCRE2 (JIT) | 165 ns | ×3.3 | 0 |
| CTRE | 50 ns | ×1 | 0 |
L'alternation à 7 branches est bien gérée par tous les moteurs sauf std::regex, dont le moteur à backtracking explore les branches séquentiellement.
Pattern : (a+)+b
Entrée : 25 caractères a sans b final (cas de non-correspondance maximisant le backtracking)
| Moteur | Temps/op | Comportement |
|---|---|---|
std::regex |
> 5 000 ms | ❌ Explosion exponentielle |
| RE2 | 85 ns | ✅ Linéaire — rejet immédiat |
| PCRE2 (JIT) | 220 ns | ✅ Rapide mais explorera davantage sur des entrées plus longues |
| PCRE2 (JIT, match_limit=10000) | 180 ns | ✅ Borné par la limite |
| CTRE | 15 ns | ✅ Rejet instantané |
Ce profil illustre le danger fondamental de std::regex : sur 25 caractères, le temps dépasse 5 secondes. Sur 30 caractères, il dépasserait plusieurs minutes. CTRE et RE2 restent en nanosecondes car ils ne sont pas sujets au backtracking exponentiel. PCRE2 gère ce pattern spécifique rapidement grâce à ses optimisations internes, mais sans la garantie formelle de RE2 sur tous les patterns pathologiques.
Les benchmarks précédents mesurent le matching. Le coût de compilation du pattern est un facteur distinct, pertinent quand les patterns sont construits dynamiquement :
| Moteur | Compilation d'un pattern simple | Compilation d'un pattern complexe |
|---|---|---|
std::regex |
~15 µs | ~80 µs |
| RE2 | ~3 µs | ~20 µs |
| PCRE2 (sans JIT) | ~2 µs | ~12 µs |
| PCRE2 (avec JIT) | ~25 µs | ~100 µs |
| CTRE | 0 ns (compile-time) | 0 ns (compile-time) |
PCRE2 avec JIT a le coût de compilation le plus élevé des moteurs runtime, car l'étape JIT génère du code machine natif. Cet investissement est amorti dès la première dizaine de matchings. Pour un pattern utilisé une seule fois sur un texte court, PCRE2 sans JIT ou RE2 ont un meilleur ratio coût-de-compilation/bénéfice.
| Moteur | Pattern simple | Pattern complexe |
|---|---|---|
std::regex |
~4-8 Ko | ~20-50 Ko |
| RE2 | ~2-4 Ko | ~8-16 Ko |
| PCRE2 (sans JIT) | ~1-2 Ko | ~4-8 Ko |
| PCRE2 (avec JIT) | ~4-8 Ko | ~12-24 Ko |
| CTRE | 0 Ko (code dans .text) | 0 Ko (code dans .text) |
| Moteur | Allocations/op (3 captures) | Notes |
|---|---|---|
std::regex |
5-10 | smatch alloue pour chaque sous-match |
| RE2 | 0 | Extraction dans des variables pré-existantes |
| PCRE2 | 0 | match_data pré-alloué, réutilisable |
| CTRE | 0 | Résultats sur la pile, vues sur l'entrée |
L'absence d'allocations par opération est un avantage critique pour les trois alternatives. Dans un serveur traitant 100 000 requêtes par seconde, chacune impliquant 5 regex matchings, les 500 000 à 1 000 000 allocations par seconde de std::regex génèrent une pression significative sur l'allocateur et le garbage collector (fragmentation, contention sur le heap en multi-thread).
L'ajout d'une bibliothèque regex augmente la taille de l'exécutable final. Voici les ordres de grandeur mesurés avec un linkage statique (-static) sur GCC 15 :
| Configuration | Taille binaire (stripped) |
|---|---|
| Programme minimal sans regex | ~25 Ko |
+ std::regex (1 pattern) |
~450 Ko |
| + RE2 (1 pattern) | ~1.2 Mo |
| + PCRE2 avec JIT (1 pattern) | ~650 Ko |
| + CTRE (1 pattern) | ~35 Ko |
| + CTRE (10 patterns distincts) | ~80 Ko |
CTRE a l'empreinte la plus faible car il génère du code spécialisé sans embarquer de moteur d'exécution généraliste. std::regex est modéré en taille mais embarque le moteur NFA complet de la libstdc++. RE2 est le plus volumineux à cause de son infrastructure DFA et de son support Unicode complet.
Pour les images Docker distroless (section 37.5) ou les environnements embarqués, cette différence peut être significative.
Temps moyen par opération (ns) — échelle logarithmique
Pattern : extraction date+heure dans une ligne de log de 120 caractères
CTRE ████ 30 ns
PCRE2 (JIT) ██████████ 110 ns
RE2 ████████████ 145 ns
PCRE2 (interp)████████████████████████████ 340 ns
std::regex ████████████████████████████████████████████████████████ 2 750 ns
|-------------|-------------|-------------|-------------|
10 100 1 000 10 000
| Situation | Recommandation | Justification |
|---|---|---|
| Pattern littéral, C++20+, performance critique | CTRE | Zéro coût runtime, zéro allocation, immunité backtracking |
| Pattern littéral, C++20+, performance non critique | CTRE | Même sans besoin de perf, la détection d'erreurs à la compilation vaut le coup |
| Pattern dynamique, entrées non fiables | RE2 | Garantie temps linéaire, protection ReDoS native |
| Pattern dynamique, fonctionnalités Perl requises | PCRE2 (JIT) | Seul moteur supportant backreferences, lookbehind variable, récursion |
| Pattern dynamique, perf non critique, zéro dépendance | std::regex |
Acceptable si le pattern et les entrées sont contrôlés |
| Prototype rapide, script ponctuel | std::regex |
Simplicité, aucune dépendance à ajouter |
| Serveur haute performance, patterns mixtes | CTRE + RE2 | CTRE pour les patterns statiques, RE2 pour les dynamiques |
| Parsing de structures imbriquées (HTML, JSON-like) | PCRE2 | Récursion (?R) nécessaire |
| Projet bloqué sur C++17 ou antérieur | RE2 ou PCRE2 | CTRE requiert C++20 |
| Image Docker minimale, binaire compact | CTRE | ~35 Ko vs ~1.2 Mo pour RE2 |
Le pattern est-il un littéral dans le code source ?
├── OUI
│ └── C++20 disponible ?
│ ├── OUI → CTRE
│ └── NON → RE2 (ou std::regex si zéro dépendance requise)
│
└── NON (dynamique)
└── Le pattern peut-il venir d'une source non fiable ?
├── OUI → RE2
└── NON
└── Besoin de backreferences, lookbehind, récursion ?
├── OUI → PCRE2 (JIT)
└── NON → RE2 (plus simple) ou PCRE2 (JIT, plus rapide sur match courts)
Agent de monitoring / collecteur de logs — Les patterns sont connus à l'avance (formats de log prédéfinis), le débit est critique (millions de lignes par seconde). CTRE est le choix naturel, complété par RE2 si certains patterns sont configurables par l'utilisateur.
API web / serveur HTTP — Les entrées proviennent de clients non fiables. Toute regex appliquée aux données utilisateur doit utiliser RE2 pour éliminer le risque ReDoS. Les patterns internes (routing, validation) peuvent utiliser CTRE.
Outil CLI de recherche (type grep) — Le pattern est fourni par l'utilisateur sur la ligne de commande. RE2 est le choix par défaut. Si l'utilisateur a besoin de fonctionnalités Perl avancées, PCRE2 avec des limites de backtracking configurées est l'alternative — c'est d'ailleurs le choix fait par ripgrep (RE2-like via le crate regex de Rust) et grep -P (PCRE2).
Pipeline ETL / data processing — Parsing de fichiers CSV, JSON, ou de dumps SQL avec des formats fixes. Les patterns sont statiques et le volume de données est élevé. CTRE pour la performance, ou PCRE2 JIT si certains patterns nécessitent des fonctionnalités avancées.
Système embarqué / binaire compact — Contraintes de taille du binaire et potentiellement pas de linkage dynamique disponible. CTRE ajoute quelques kilo-octets au segment .text. RE2 ajoute plus d'un méga-octet. std::regex est un compromis intermédiaire si déjà linkée via la libstdc++.
Avant de choisir un moteur regex sur la base de benchmarks, vérifier que la regex est réellement le goulot d'étranglement. Un profiling avec perf (section 31.1) ou des flamegraphs (section 31.3) doit confirmer que le temps passé dans le matching est significatif par rapport au reste du traitement. Si le programme passe 95 % de son temps en I/O réseau, optimiser la regex de 30 ns à 15 ns n'a aucun impact mesurable.
Les expressions régulières ne sont pas toujours le bon outil. Pour parser du JSON, utiliser nlohmann/json (section 24.1). Pour du YAML, utiliser yaml-cpp (section 24.2). Pour du XML, utiliser pugixml (section 24.4). Pour des formats binaires, utiliser Protobuf ou FlatBuffers (chapitre 25). Les regex sont appropriées pour du texte semi-structuré ou des validations ponctuelles, pas pour des formats avec une grammaire formelle complète.
C'est l'erreur de benchmark la plus fréquente. Inclure la construction du std::regex ou de l'objet RE2 dans la boucle de mesure fausse les résultats en ajoutant le coût de compilation à chaque itération. Sauf si l'objectif est précisément de mesurer le coût de compilation, le pattern doit être construit avant la boucle.
Un benchmark qui ne mesure que le temps peut masquer un problème d'allocations. Un moteur qui fait zéro allocation par opération se comporte beaucoup mieux sous contention multi-thread et sous pression mémoire qu'un moteur qui en fait 5 à 10. Utiliser benchmark::State::SetBytesProcessed et compléter avec Valgrind/Massif pour un tableau complet.
PCRE2 avec des limites de backtracking configurées (match_limit, depth_limit) et des patterns audités offre une sécurité raisonnable pour la plupart des cas d'usage. Le risque ReDoS est surtout critique quand les patterns eux-mêmes sont fournis par des sources non fiables — un cas où seul RE2 offre une garantie formelle. Quand le pattern est contrôlé par le développeur et que seules les entrées sont variables, PCRE2 avec limites est un choix parfaitement défendable.
CTRE requiert C++20 avec un bon support des class-type NTTP. GCC 15 et Clang 20 supportent cela pleinement, mais des compilateurs plus anciens ou certains environnements cross-compilation peuvent poser problème. RE2 et PCRE2 sont des bibliothèques C/C++ classiques qui compilent sur pratiquement tout.
Avant d'intégrer une solution regex dans un projet, passer en revue cette liste :
- Le problème nécessite-t-il vraiment une regex ? Un
std::string::find, unstd::from_charsou un parser dédié pourrait suffire. - Le pattern est-il statique ou dynamique ? Statique → évaluer CTRE. Dynamique → RE2 ou PCRE2.
- Les entrées sont-elles fiables ? Non fiables → RE2 obligatoire (ou PCRE2 avec limites strictes).
- Des fonctionnalités Perl avancées sont-elles requises ? Oui → PCRE2. Non → RE2 ou CTRE.
- La performance est-elle critique sur ce chemin de code ? Profiler avant d'optimiser.
- Le projet peut-il ajouter une dépendance externe ? Non →
std::regexen dernier recours, avec les précautions de la section 24.6.1. - L'environnement cible supporte-t-il C++20 ? Non → CTRE n'est pas une option.
- Le binaire a-t-il des contraintes de taille ? Oui → CTRE (~Ko) >> PCRE2 (~650 Ko) >> RE2 (~1.2 Mo).
- Le pattern est-il pré-compilé et réutilisé ? Toujours pré-compiler. Jamais dans une boucle.
- Les limites de backtracking sont-elles configurées ? Pour PCRE2, toujours configurer
match_limitsi les entrées sont variables.
💡 Le mot de la fin : le C++ offre en 2026 un écosystème regex mature avec des solutions adaptées à chaque contexte. La recommandation par défaut est CTRE pour les patterns statiques et RE2 pour les patterns dynamiques. PCRE2 est le recours pour les fonctionnalités avancées.
std::regexreste un dernier recours quand aucune dépendance externe n'est envisageable. Quel que soit le choix, la règle d'or reste la même : mesurer avant d'optimiser, et ne jamais exposer un moteur à backtracking à des entrées non fiables.