web-dev-qa-db-fra.com

Pourquoi «\ s +» est-il tellement plus rapide que «\ s \ s *» dans ce regex Perl?

Pourquoi le remplacement de \s* (Ou même \s\s*) Par \s+ Entraîne-t-il une telle accélération pour cette entrée?

use Benchmark qw(:all);
$x=(" " x 100000) . "_\n";
$count = 100;
timethese($count, {
    '/\s\s*\n/' => sub { $x =~ /\s\s*\n/ },
    '/\s+\n/' => sub { $x =~ /\s+\n/ },
});

Lien vers la version en ligne

J'ai remarqué une expression régulière lente s/\s*\n\s*/\n/g Dans mon code - lorsqu'on m'a donné un fichier d'entrée de 450 Ko composé de beaucoup d'espaces avec quelques non-espaces ici et là, et une nouvelle ligne à la fin - l'expression régulière s'est bloquée et n'a jamais fini .

J'ai intuitivement remplacé l'expression régulière par s/\s+\n/\n/g; s/\n\s+/\n/g; Et tout allait bien.

Mais pourquoi est-ce tellement plus rapide? Après avoir utilisé re Debug => "EXECUTE", J'ai remarqué que la version \s+ Est en quelque sorte optimisée pour fonctionner en une seule itération: http://Pastebin.com/0Ug6xPiQ

Matching REx "\s*\n" against "       _%n"
Matching stclass ANYOF{i}[\x09\x0a\x0c\x0d ][{non-utf8-latin1-all}{unicode_all}] against "       _%n" (9 bytes)
   0 <> <       _%n>         |  1:STAR(3)
                                  SPACE can match 7 times out of 2147483647...
                                  failed...
   1 < > <      _%n>         |  1:STAR(3)
                                  SPACE can match 6 times out of 2147483647...
                                  failed...
   2 <  > <     _%n>         |  1:STAR(3)
                                  SPACE can match 5 times out of 2147483647...
                                  failed...
   3 <   > <    _%n>         |  1:STAR(3)
                                  SPACE can match 4 times out of 2147483647...
                                  failed...
   4 <    > <   _%n>         |  1:STAR(3)
                                  SPACE can match 3 times out of 2147483647...
                                  failed...
   5 <     > <  _%n>         |  1:STAR(3)
                                  SPACE can match 2 times out of 2147483647...
                                  failed...
   6 <      > < _%n>         |  1:STAR(3)
                                  SPACE can match 1 times out of 2147483647...
                                  failed...
   8 <       _> <%n>         |  1:STAR(3)
                                  SPACE can match 1 times out of 2147483647...
   8 <       _> <%n>         |  3:  EXACT <\n>(5)
   9 <       _%n> <>         |  5:  END(0)
Match successful!
Matching REx "\s+\n" against "       _%n"
Matching stclass SPACE against "       _" (8 bytes)
   0 <> <       _%n>         |  1:PLUS(3)
                                  SPACE can match 7 times out of 2147483647...
                                  failed...

Je sais que Perl 5.10+ échouera immédiatement le regex (sans l'exécuter) si aucune nouvelle ligne n'est présente. Je soupçonne qu'il utilise l'emplacement de la nouvelle ligne pour réduire la quantité de recherches qu'il fait. Pour tous les cas ci-dessus, il semble réduire intelligemment le retour en arrière impliqué (généralement /\s*\n/ Contre une chaîne d'espaces prendrait un temps exponentiel). Quelqu'un peut-il expliquer pourquoi la version \s+ Est tellement plus rapide?

Notez également que \s*? N'offre aucune accélération.

56
rjh

Lorsqu'il existe un nœud "plus" (par exemple \s+) au début d'un modèle et le nœud ne correspond pas, le moteur d'expression régulière avance jusqu'au point d'échec et réessaye; avec \s*, d'un autre côté, le moteur ne fait avancer qu'un caractère à la fois.

Yves Orton explique bien cette optimisation ici :

L'optimisation de la classe de départ a deux modes, "essayer toutes les positions de départ valides" (doevery) et "flip flop mode" (! Doevery) où elle n'essaye que la première position de départ valide dans une séquence.

Considérez/(\ d +) X/et la chaîne "123456Y", nous savons maintenant que si nous ne parvenons pas à faire correspondre X après avoir trouvé "123456", nous ne parviendrons pas non plus à faire correspondre après "23456" (en supposant qu'aucun mauvais tour n'est en place, qui désactivent l'optimisation de toute façon), nous savons donc que nous pouvons avancer jusqu'à ce que la vérification/échoue/et seulement ensuite commencer à chercher une vraie correspondance. C'est le mode flip-flop.

/\s+/ déclenche le mode bascule; /\s*/, /\s\s*/, et /\s\s+/ pas. Cette optimisation ne peut pas être appliquée aux nœuds "étoiles" comme \s* car ils peuvent correspondre à zéro caractère, donc un échec à un moment donné d'une séquence n'est pas indicatif d'un échec plus tard dans la même séquence.


Vous pouvez le voir dans la sortie de débogage pour chaque expression régulière. J'ai mis en surbrillance les caractères ignorés avec ^. Comparez ceci (saute quatre caractères à la fois):

$ Perl -Mre=Debug,MATCH -e'"123 456 789 x" =~ /\d+x/'
   ...
   0 <> <123 456 78>         |  1:PLUS(3)
                                  POSIXD[\d] can match 3 times out of 2147483647...
                                  failed...
   4 <123 > <456 789 x>      |  1:PLUS(3)
      ^^^^
                                  POSIXD[\d] can match 3 times out of 2147483647...
                                  failed...
   8 <23 456 > <789 x>       |  1:PLUS(3)
         ^^^^
                                  POSIXD[\d] can match 3 times out of 2147483647...
                                  failed...

à ceci (saute un ou deux caractères à la fois):

$ Perl -Mre=Debug,MATCH -e'"123 456 789 x" =~ /\d*x/'
   ...
   0 <> <123 456 78>         |  1:STAR(3)
                                  POSIXD[\d] can match 3 times out of 2147483647...
                                  failed...
   1 <1> <23 456 789>        |  1:STAR(3)
      ^
                                  POSIXD[\d] can match 2 times out of 2147483647...
                                  failed...
   2 <12> <3 456 789 >       |  1:STAR(3)
       ^
                                  POSIXD[\d] can match 1 times out of 2147483647...
                                  failed...
   4 <123 > <456 789 x>      |  1:STAR(3)
        ^^
                                  POSIXD[\d] can match 3 times out of 2147483647...
                                  failed...
   5 <123 4> <56 789 x>      |  1:STAR(3)
          ^
                                  POSIXD[\d] can match 2 times out of 2147483647...
                                  failed...
   6 <23 45> <6 789 x>       |  1:STAR(3)
          ^
                                  POSIXD[\d] can match 1 times out of 2147483647...
                                  failed...
   8 <23 456 > <789 x>       |  1:STAR(3)
           ^^
                                  POSIXD[\d] can match 3 times out of 2147483647...
                                  failed...
   9 <23 456 7> <89 x>       |  1:STAR(3)
             ^
                                  POSIXD[\d] can match 2 times out of 2147483647...
                                  failed...
  10 <23 456 78> <9 x>       |  1:STAR(3)
              ^
                                  POSIXD[\d] can match 1 times out of 2147483647...
                                  failed...
  12 <23 456 789 > <x>       |  1:STAR(3)
               ^^
                                  POSIXD[\d] can match 0 times out of 2147483647...
  12 <23 456 789 > <x>       |  3:  EXACT <x>(5)
  13 <23 456 789 x> <>       |  5:  END(0)

Notez que l'optimisation n'est pas appliquée à /\s\s+/, car \s+ n'est pas au début du motif. Tous les deux /\s\s+/ (logiquement équivalent à /\s{2,}/) et /\s\s*/ (logiquement équivalent à /\s+/) probablement pourrait être optimisé, cependant; il pourrait être judicieux de demander à Perl5-porters si cela en vaut la peine.


Si vous êtes intéressé, le "mode flip-flop" est activé en définissant le PREGf_SKIP drapeau sur une expression régulière lorsqu'elle est compilée. Voir le code autour des lignes 7344 et 7405 dans regcomp.c et la ligne 1585 dans regexec.c dans la source 5.24.0.

20
ThisSuitIsBlackNot

Tout d'abord, même si l'expression régulière résultante ne conserve pas la même signification, réduisons l'expression régulière à \s*0 et \s+0 et utilise (" " x 4) . "_0" en entrée. Pour les sceptiques, vous pouvez voir ici que le décalage est toujours présent.

Considérons maintenant le code suivant:

$x = (" " x 4) . "_ 0";
$x =~ /\s*0/; # The slow line 
$x =~ /\s+0/; # The fast line

Creuser un peu avec use re debugcolor; nous obtenons la sortie suivante:

Guessing start of match in sv for REx "\s*0" against "    _0"
Found floating substr "0" at offset 5...
start_shift: 0 check_at: 5 s: 0 endpos: 6 checked_upto: 0
Does not contradict STCLASS...
Guessed: match at offset 0
Matching REx "\s*0" against "    _0"
Matching stclass ANYOF_SYNTHETIC[\x09-\x0d 0\x85\xa0][{unicode_all}] against "    _0" (6 bytes)
   0 <    _0>|  1:STAR(3)
                                  POSIXD[\s] can match 4 times out of 2147483647...
                                  failed...
   1 <    _0>|  1:STAR(3)
                                  POSIXD[\s] can match 3 times out of 2147483647...
                                  failed...
   2 <    _0>|  1:STAR(3)
                                  POSIXD[\s] can match 2 times out of 2147483647...
                                  failed...
   3 <    _0>|  1:STAR(3)
                                  POSIXD[\s] can match 1 times out of 2147483647...
                                  failed...
   5 <    _0>|  1:STAR(3)
                                  POSIXD[\s] can match 0 times out of 2147483647...
   5 <    _0>|  3:  EXACT <0>(5)
   6 <    _0>|  5:  END(0)
Match successful!

-----------------------

Guessing start of match in sv for REx "\s+0" against "    _0"
Found floating substr "0" at offset 5...
start_shift: 1 check_at: 5 s: 0 endpos: 5 checked_upto: 0
Does not contradict STCLASS...
Guessed: match at offset 0
Matching REx "\s+0" against "    _0"
Matching stclass POSIXD[\s] against "    _" (5 bytes)
   0 <    _0>|  1:PLUS(3)
                                  POSIXD[\s] can match 4 times out of 2147483647...
                                  failed...
Contradicts stclass... [regexec_flags]
Match failed

Perl semble être optimisé pour l'échec . Il recherchera d'abord les chaînes constantes (qui ne consomment que O (N)). Ici, il recherchera 0: Found floating substr "0" at offset 5...

Ensuite, cela commencera par la partie de l'expression rationnelle, respectivement \s* et \s+, par rapport à toute la chaîne minimale à vérifier:

Matching REx "\s*0" against "    _0"
Matching stclass ANYOF_SYNTHETIC[\x09-\x0d 0\x85\xa0][{unicode_all}] against "    _0" (6 bytes)
Matching REx "\s+0" against "    _0"
Matching stclass POSIXD[\s] against "    _" (5 bytes) # Only 5 bytes because there should be at least 1 "\s" char

Après cela, il recherchera le premier poste répondant à l'exigence stclass, ici à la position 0.

  • \s*0:
    • commence à 0, trouve 4 espaces puis échoue;
    • commence à 1, trouve 3 espaces puis échoue;
    • commence à 2, trouve 2 espaces puis échoue;
    • commence à 3, trouve 1 espace puis échoue;
    • commence à 4, trouvez 0 espace puis n'échoue pas ;
    • Trouver un 0
  • \s+0:
    • commence à 0, trouve 4 espaces puis échoue. Comme le nombre minimum d'espaces ne correspond pas, l'expression régulière échoue instantanément.

Si vous voulez vous amuser avec l'optimisation des expressions rationnelles Perl, vous pouvez envisager les expressions régulières suivantes / *\n et / * \n. À première vue, ils se ressemblent, ont la même signification ... Mais si vous courez contre (" " x 40000) . "_\n" le premier vérifiera toutes les possibilités tandis que le second cherchera " \n" et échoue immédiatement.

Dans un moteur regex Vanilla, non optimisé, les deux regex peuvent provoquer un retour en arrière catastrophique, car ils doivent réessayer le modèle au fur et à mesure. Cependant, dans l'exemple ci-dessus, le second n'échoue pas avec Perl car il a été optimisé pour find floating substr "0%n"


Vous pouvez voir un autre exemple sur le blog de Jeff Atwood .

Notez également que le problème ne concerne pas \s considération mais tout schéma où xx* est utilisé à la place de x+ voir exemple avec 0s et aussi quantificateurs explosifs regex

Avec un exemple aussi court, le comportement est "trouvable", mais si vous commencez à jouer avec des motifs compliqués, il est loin d'être facile à repérer, par exemple: L'expression régulière bloque le programme (100% d'utilisation du processeur)

28
Thomas Ayoub

Le \s+\n Requiert que le caractère précédant le \n Soit un SPACE.

Selon use re qw(debug) la compilation établit qu'elle a besoin d'une chaîne droite d'un nombre connu d'espaces, jusqu'à la sous-chaîne \n, Qui est d'abord vérifiée en entrée. Ensuite, il vérifie la sous-chaîne d'espace uniquement de longueur fixe par rapport à la partie restante de l'entrée, échouant en ce qui concerne _. C'est une seule possibilité de vérifier, quel que soit le nombre d'espaces de l'entrée. (Lorsqu'il y a plus de _\n, Chacun se révèle échouer directement de la même manière, par sortie de débogage.)

Vu sous cet angle, c'est une optimisation à laquelle vous vous attendez presque, en utilisant un modèle de recherche plutôt spécifique et en tirant parti de cette entrée. Sauf lorsqu'ils sont pris en comparaison avec d'autres moteurs, qui ne font clairement pas ce genre d'analyse.

Avec \s*\n Ce n'est pas le cas. Une fois que \n Est trouvé et que le caractère précédent n'est pas un espace, la recherche n'a pas échoué puisque \s* N'autorise rien (zéro caractère). Il n'y a pas non plus de sous-chaînes de longueur fixe, et c'est dans le jeu de retour en arrière.

14
zdim

Je ne suis pas sûr de l'intérieur du moteur d'expression régulière, mais il semble qu'il ne reconnaisse pas que \s+ Est en quelque sorte identique as \s\s*, car dans le second, il correspond à un espace, puis essaie de faire correspondre un nombre toujours croissant d'espaces, tandis que dans le premier, il conclut immédiatement qu'il n'y a pas rencontre.

La sortie utilisant use re qw( Debug ); le montre clairement, en utilisant une chaîne beaucoup plus courte:

test_re.pl

#!/usr/bin/env Perl
use re qw(debug);

$x=(" " x 10) . "_\n";
print '-'x50 . "\n";
$x =~ /\s+\n/;
print '-'x50 . "\n";
$x =~ /\s\s*\n/;
print '-'x50 . "\n";

Sortie

Compiling REx "\s+\n"
Final program:
    1: PLUS (3)
    2:   SPACE (0)
    3: EXACT <\n> (5)
    5: END (0)
floating "%n" at 1..2147483647 (checking floating) stclass SPACE plus minlen 2
Compiling REx "\s\s*\n"
Final program:
    1: SPACE (2)
    2: STAR (4)
    3:   SPACE (0)
    4: EXACT <\n> (6)
    6: END (0)
floating "%n" at 1..2147483647 (checking floating) stclass SPACE minlen 2
--------------------------------------------------
Guessing start of match in sv for REx "\s+\n" against "          _%n"
Found floating substr "%n" at offset 11...
    start_shift: 1 check_at: 11 s: 0 endpos: 11
Does not contradict STCLASS...
Guessed: match at offset 0
Matching REx "\s+\n" against "          _%n"
Matching stclass SPACE against "          _" (11 bytes)
   0 <> <          >         |  1:PLUS(3)
                                  SPACE can match 10 times out of 2147483647...
                                  failed...
Contradicts stclass... [regexec_flags]
Match failed
--------------------------------------------------
Guessing start of match in sv for REx "\s\s*\n" against "          _%n"
Found floating substr "%n" at offset 11...
    start_shift: 1 check_at: 11 s: 0 endpos: 11
Does not contradict STCLASS...
Guessed: match at offset 0
Matching REx "\s\s*\n" against "          _%n"
Matching stclass SPACE against "          _" (11 bytes)
   0 <> <          >         |  1:SPACE(2)
   1 < > <         _>        |  2:STAR(4)
                                  SPACE can match 9 times out of 2147483647...
                                  failed...
   1 < > <         _>        |  1:SPACE(2)
   2 <  > <        _>        |  2:STAR(4)
                                  SPACE can match 8 times out of 2147483647...
                                  failed...
   2 <  > <        _>        |  1:SPACE(2)
   3 <   > <       _%n>      |  2:STAR(4)
                                  SPACE can match 7 times out of 2147483647...
                                  failed...
   3 <   > <       _%n>      |  1:SPACE(2)
   4 <    > <      _%n>      |  2:STAR(4)
                                  SPACE can match 6 times out of 2147483647...
                                  failed...
   4 <    > <      _%n>      |  1:SPACE(2)
   5 <     > <     _%n>      |  2:STAR(4)
                                  SPACE can match 5 times out of 2147483647...
                                  failed...
   5 <     > <     _%n>      |  1:SPACE(2)
   6 <      > <    _%n>      |  2:STAR(4)
                                  SPACE can match 4 times out of 2147483647...
                                  failed...
   6 <      > <    _%n>      |  1:SPACE(2)
   7 <       > <   _%n>      |  2:STAR(4)
                                  SPACE can match 3 times out of 2147483647...
                                  failed...
   7 <       > <   _%n>      |  1:SPACE(2)
   8 <        > <  _%n>      |  2:STAR(4)
                                  SPACE can match 2 times out of 2147483647...
                                  failed...
   8 <        > <  _%n>      |  1:SPACE(2)
   9 <         > < _%n>      |  2:STAR(4)
                                  SPACE can match 1 times out of 2147483647...
                                  failed...
   9 <         > < _%n>      |  1:SPACE(2)
  10 <          > <_%n>      |  2:STAR(4)
                                  SPACE can match 0 times out of 2147483647...
                                  failed...
Contradicts stclass... [regexec_flags]
Match failed
--------------------------------------------------
Freeing REx: "\s+\n"
Freeing REx: "\s\s*\n"
7
xxfelixxx