Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 
 
 

README.md

Pourquoi std::regex perd contre le module re de Python

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 Python

Machine de référence : Windows 11, MSVC 19.29 (/O2), CPython 3.9.13, texte de logs de 296 Ko contenant 4 000 horodatages.


1. regex_probe — où part le temps ?

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::smatch au lieu de sregex_iterator ne change rien (140 ms).
  • Ce ne sont pas les groupes de capture. Les supprimer ne change rien non plus (157 ms).

2. prefilter — le moteur sait-il sauter les positions impossibles ?

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.

3. perpos — d'où vient 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.


Ce qu'il faut en retenir

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.