Le contre-test regex du benchmark principal donne un résultat contre-intuitif : Python y est ~16× plus rapide que C++. Ces trois sondes décomposent le temps pour comprendre d'où vient l'écart, plutôt que de l'affirmer.
build.bat :: compile et exécute les trois sondes C++, puis les deux sondes PythonMachine de référence : Windows 11, MSVC 19.29 (/O2), CPython 3.9.13, texte de logs
de 296 Ko contenant 4 000 horodatages.
Temps d'une passe complète de comptage sur le texte de logs :
| Implémentation | Temps |
|---|---|
| C++, scanner écrit à la main | 1,0 ms |
Python re.finditer |
7,5 ms |
C++ std::regex, motif sans {n} |
51 ms |
C++ std::regex, motif du benchmark |
117–140 ms |
| Python, scanner écrit à la main | 226 ms |
Deux hypothèses intuitives sont éliminées d'emblée :
- Ce n'est pas l'allocation des objets de match. Réutiliser un
std::smatchau lieu desregex_iteratorne change rien (140 ms). - Ce ne sont pas les groupes de capture. Les supprimer ne change rien non plus (157 ms).
Même longueur de texte, mais sans aucun chiffre : zéro match possible, donc tout le temps mesuré est du coût de rejet pur.
| Temps | |
|---|---|
std::regex |
143,9 ms |
re.finditer |
7,0 ms |
| boucle C++ triviale comptant les chiffres | 0,4 ms |
Les deux moteurs mettent le même temps que sur le texte réel : aucun des deux ne saute les positions impossibles, tous deux tentent partout. L'écart n'est donc pas dans la stratégie de recherche, mais dans le coût d'un rejet.
Coût par position testée, sur le texte sans chiffre :
| Motif | ns/position |
|---|---|
\d |
3,4 |
\d\d\d\d |
4,6 |
| motif complet, sans quantificateur | 2,2 |
\d{1} |
589 |
\d{4} |
445 |
| motif complet, avec quantificateurs | 439 |
\d{1} et \d reconnaissent exactement le même langage, et il y a un facteur 170
entre eux. Ce n'est donc ni la longueur du motif, ni les captures, ni la taille du
texte : dès qu'un quantificateur {n} apparaît, std::regex bascule sur son nœud
générique de répétition avec backtracking, et en paie l'installation à chaque position
de départ — avant même d'avoir testé le premier caractère.
Sur le texte de logs réel, réécrire le motif sans {n} ne rapporte que ×2,3
(117 → 51 ms), parce que le texte est plein de chiffres : beaucoup de positions passent
le premier test et engagent le moteur de toute façon. Les ×7 restants face à Python
viennent de la différence de conception : re compile vers un bytecode compact exécuté
par une boucle C serrée, std::regex construit un graphe de nœuds alloués sur le tas et
le parcourt par déréférencements.
Le ×16 est réel, mais il compare deux bibliothèques standard, pas deux plafonds de performance. La diagonale est le vrai enseignement :
| bibliothèque standard | boucle écrite à la main | |
|---|---|---|
| Python | 7,5 ms | 226 ms |
| C++ | 140 ms | 1,0 ms |
Python est rapide quand il délègue et lent quand il boucle ; le C++ est l'inverse. Le langage ne compte que là où le langage fait le travail.
std::regex traîne cette réputation pour des raisons historiques : portage de
Boost.Regex standardisé en C++11, construction du motif obligatoirement à l'exécution,
et surtout un ABI gelé qui interdit aux implémentations de changer la disposition
mémoire de leurs types. Les optimisations profondes y sont devenues impossibles à
introduire. Un C++ moderne utiliserait RE2, PCRE2, ou CTRE — cette dernière compilant le
motif à la compilation via des templates, ce qui ramène dans les ordres de grandeur du
scanner écrit à la main.