Il modo più veloce per verificare se una stringa corrisponde a un'espressione regolare in ruby?


96

Qual è il modo più veloce per verificare se una stringa corrisponde a un'espressione regolare in Ruby?

Il mio problema è che devo "egrep" attraverso un enorme elenco di stringhe per trovare quali sono quelle che corrispondono a un'espressione regolare che viene fornita in fase di esecuzione. Mi interessa solo se la stringa corrisponde alla regexp, non dove corrisponde, né quale sia il contenuto dei gruppi corrispondenti. Spero che questa ipotesi possa essere utilizzata per ridurre la quantità di tempo che il mio codice impiega per la corrispondenza delle espressioni regolari.

Carico la regexp con

pattern = Regexp.new(ptx).freeze

Ho scoperto che string =~ patternè leggermente più veloce di string.match(pattern).

Esistono altri trucchi o scorciatoie che possono essere utilizzati per rendere questo test ancora più veloce?


Se non ti interessa il contenuto dei gruppi corrispondenti, perché li hai? Puoi rendere la regex più veloce convertendola in non-cattura.
Mark Thomas

1
Poiché la regexp viene fornita in fase di esecuzione, presumo che non sia vincolata, nel qual caso potrebbero esserci riferimenti interni all'interno del reg-exp ai raggruppamenti, e quindi convertirli in non-capturing modificando la regexp potrebbe modificare il risultato (a meno che tu verificare inoltre i riferimenti interni, ma il problema diventa sempre più complesso). Lo trovo curioso = ~ sarebbe più veloce di string.match.
djconnel

qual è il vantaggio di congelare la regexp qui?
Hardik

Risposte:


103

A partire da Ruby 2.4.0, puoi usare RegExp#match?:

pattern.match?(string)

Regexp#match?è esplicitamente elencato come un miglioramento delle prestazioni nelle note di rilascio per 2.4.0 , poiché evita le allocazioni di oggetti eseguite da altri metodi come Regexp#matche =~:

Regexp # corrisponde?
Aggiunto Regexp#match?, che esegue una corrispondenza regexp senza creare un oggetto di riferimento posteriore e modificarlo $~per ridurre l'allocazione degli oggetti.


5
Grazie per il suggerimento. Ho aggiornato lo script di benchmark ed Regexp#match?è effettivamente almeno il 50% più veloce rispetto alle altre alternative.
gioele

74

Questo è un semplice benchmark:

require 'benchmark'

"test123" =~ /1/
=> 4
Benchmark.measure{ 1000000.times { "test123" =~ /1/ } }
=>   0.610000   0.000000   0.610000 (  0.578133)

"test123"[/1/]
=> "1"
Benchmark.measure{ 1000000.times { "test123"[/1/] } }
=>   0.718000   0.000000   0.718000 (  0.750010)

irb(main):019:0> "test123".match(/1/)
=> #<MatchData "1">
Benchmark.measure{ 1000000.times { "test123".match(/1/) } }
=>   1.703000   0.000000   1.703000 (  1.578146)

Quindi =~è più veloce, ma dipende da cosa vuoi avere come valore restituito. Se vuoi solo controllare se il testo contiene una regex o non usa=~


2
Come ho scritto, ho già scoperto che =~è più veloce di match, con un aumento delle prestazioni meno drammatico quando si opera su espressioni regolari più grandi. Quello che mi chiedo è se esiste uno strano modo per rendere questo controllo ancora più veloce, magari sfruttando qualche strano metodo in Regexp o qualche strano costrutto.
gioele

Penso che non ci siano altre soluzioni
Dougui il

Di cosa !("test123" !~ /1/)?
ma11hew28

1
@ MattDiPasquale, due volte l'inverso non dovrebbe essere più veloce di"test123" =~ /1/
Dougui

1
/1/.match?("test123")è più veloce che "test123" =~ /1/se fosse solo per controllare se il testo contiene una regex o meno.
noraj

41

Questo è il benchmark che ho eseguito dopo aver trovato alcuni articoli in rete.

Con 2.4.0 il vincitore è re.match?(str)(come suggerito da @ wiktor-stribiżew), nelle versioni precedenti, re =~ strsembra essere il più veloce, anche se str =~ reè quasi altrettanto veloce.

#!/usr/bin/env ruby
require 'benchmark'

str = "aacaabc"
re = Regexp.new('a+b').freeze

N = 4_000_000

Benchmark.bm do |b|
    b.report("str.match re\t") { N.times { str.match re } }
    b.report("str =~ re\t")    { N.times { str =~ re } }
    b.report("str[re]  \t")    { N.times { str[re] } }
    b.report("re =~ str\t")    { N.times { re =~ str } }
    b.report("re.match str\t") { N.times { re.match str } }
    if re.respond_to?(:match?)
        b.report("re.match? str\t") { N.times { re.match? str } }
    end
end

Risultati MRI 1.9.3-o551:

$ ./bench-re.rb  | sort -t $'\t' -k 2
       user     system      total        real
re =~ str         2.390000   0.000000   2.390000 (  2.397331)
str =~ re         2.450000   0.000000   2.450000 (  2.446893)
str[re]           2.940000   0.010000   2.950000 (  2.941666)
re.match str      3.620000   0.000000   3.620000 (  3.619922)
str.match re      4.180000   0.000000   4.180000 (  4.180083)

Risultati MRI 2.1.5:

$ ./bench-re.rb  | sort -t $'\t' -k 2
       user     system      total        real
re =~ str         1.150000   0.000000   1.150000 (  1.144880)
str =~ re         1.160000   0.000000   1.160000 (  1.150691)
str[re]           1.330000   0.000000   1.330000 (  1.337064)
re.match str      2.250000   0.000000   2.250000 (  2.255142)
str.match re      2.270000   0.000000   2.270000 (  2.270948)

Risultati MRI 2.3.3 (c'è una regex matching, sembra):

$ ./bench-re.rb  | sort -t $'\t' -k 2
       user     system      total        real
re =~ str         3.540000   0.000000   3.540000 (  3.535881)
str =~ re         3.560000   0.000000   3.560000 (  3.560657)
str[re]           4.300000   0.000000   4.300000 (  4.299403)
re.match str      5.210000   0.010000   5.220000 (  5.213041)
str.match re      6.000000   0.000000   6.000000 (  6.000465)

Risultati MRI 2.4.0:

$ ./bench-re.rb  | sort -t $'\t' -k 2
       user     system      total        real
re.match? str     0.690000   0.010000   0.700000 (  0.682934)
re =~ str         1.040000   0.000000   1.040000 (  1.035863)
str =~ re         1.040000   0.000000   1.040000 (  1.042963)
str[re]           1.340000   0.000000   1.340000 (  1.339704)
re.match str      2.040000   0.000000   2.040000 (  2.046464)
str.match re      2.180000   0.000000   2.180000 (  2.174691)

Solo per aggiungere una nota, le forme letterali sono più veloci di queste. Ad esempio /a+b/ =~ stre str =~ /a+b/. È valido anche quando le itera attraverso le funzioni e lo vedo abbastanza valido da considerarlo migliore dell'archiviazione e del congelamento di espressioni regolari su una variabile. Ho testato il mio script con ruby ​​1.9.3p547, ruby ​​2.0.0p481 e ruby ​​2.1.4p265. È possibile che questi miglioramenti siano stati apportati su patch successive, ma non ho ancora intenzione di testarlo con versioni / patch precedenti.
konsolebox

Pensavo !(re !~ str)potesse essere più veloce, ma non lo è.
ma11hew28

7

Che mi dici di re === str(case compare)?

Dal momento che restituisce true o false e non ha bisogno di memorizzare corrispondenze, restituire l'indice delle corrispondenze e cose del genere, mi chiedo se sarebbe un modo ancora più veloce di abbinare =~.


Ok, l'ho provato. =~è ancora più veloce, anche se si dispone di più gruppi di acquisizione, tuttavia è più veloce delle altre opzioni.

A proposito, a cosa serve freeze? Non ho potuto misurare alcun aumento delle prestazioni da esso.


Gli effetti di freezenon verranno visualizzati nei risultati perché si verifica prima dei loop di benchmark e agisce sul pattern stesso.
Tin Man

4

A seconda di quanto sia complicata la tua espressione regolare, potresti usare semplicemente il semplice affettamento delle stringhe. Non sono sicuro della praticità di questo per la tua applicazione o se offrirebbe effettivamente miglioramenti di velocità.

'testsentence'['stsen']
=> 'stsen' # evaluates to true
'testsentence'['koala']
=> nil # evaluates to false

Non posso utilizzare lo string slicing perché la regexp viene fornita in fase di esecuzione e non ho alcun controllo su questo.
gioele

È possibile utilizzare la suddivisione in stringhe, ma non la suddivisione in sezioni utilizzando una stringa fissa. Usa una variabile invece di una stringa tra virgolette e funzionerebbe comunque.
Tin Man

3

Quello che mi chiedo è se esiste uno strano modo per rendere questo controllo ancora più veloce, magari sfruttando qualche strano metodo in Regexp o qualche strano costrutto.

I motori di espressione regolare variano nel modo in cui implementano le ricerche, ma, in generale, ancorano i tuoi schemi per la velocità ed evitano corrispondenze avide, specialmente quando si cercano stringhe lunghe.

La cosa migliore da fare, finché non si ha familiarità con il funzionamento di un particolare motore, è eseguire benchmark e aggiungere / rimuovere ancoraggi, provare a limitare le ricerche, utilizzare caratteri jolly rispetto a corrispondenze esplicite, ecc.

La gemma fruttata è molto utile per eseguire rapidamente il benchmarking delle cose, perché è intelligente. Anche il codice Benchmark integrato di Ruby è utile, sebbene tu possa scrivere test che ti ingannano non facendo attenzione.

Ho usato entrambi in molte risposte qui su Stack Overflow, quindi puoi cercare tra le mie risposte e vedrai molti piccoli trucchi e risultati per darti idee su come scrivere codice più veloce.

La cosa più importante da ricordare è che è brutto ottimizzare prematuramente il codice prima di sapere dove si verificano i rallentamenti.


0

Per completare le risposte di Wiktor Stribiżew e Dougui , direi che /regex/.match?("string")più o meno velocemente "string".match?(/regex/).

Ruby 2.4.0 (1000000 ~ 2 sec)

2.4.0 > require 'benchmark'
 => true 
2.4.0 > Benchmark.measure{ 10000000.times { /^CVE-[0-9]{4}-[0-9]{4,}$/.match?("CVE-2018-1589") } }
 => #<Benchmark::Tms:0x005563da1b1c80 @label="", @real=2.2060338060000504, @cstime=0.0, @cutime=0.0, @stime=0.04000000000000001, @utime=2.17, @total=2.21> 
2.4.0 > Benchmark.measure{ 10000000.times { "CVE-2018-1589".match?(/^CVE-[0-9]{4}-[0-9]{4,}$/) } }
 => #<Benchmark::Tms:0x005563da139eb0 @label="", @real=2.260814556000696, @cstime=0.0, @cutime=0.0, @stime=0.010000000000000009, @utime=2.2500000000000004, @total=2.2600000000000007> 

Ruby 2.6.2 (100000000 ~ 20 sec)

irb(main):001:0> require 'benchmark'
=> true
irb(main):005:0> Benchmark.measure{ 100000000.times { /^CVE-[0-9]{4}-[0-9]{4,}$/.match?("CVE-2018-1589") } }
=> #<Benchmark::Tms:0x0000562bc83e3768 @label="", @real=24.60139879199778, @cstime=0.0, @cutime=0.0, @stime=0.010000999999999996, @utime=24.565644999999996, @total=24.575645999999995>
irb(main):004:0> Benchmark.measure{ 100000000.times { "CVE-2018-1589".match?(/^CVE-[0-9]{4}-[0-9]{4,}$/) } }
=> #<Benchmark::Tms:0x0000562bc846aee8 @label="", @real=24.634255946999474, @cstime=0.0, @cutime=0.0, @stime=0.010046, @utime=24.598276, @total=24.608321999999998>

Nota: i tempi variano, a volte /regex/.match?("string")è più veloce ea volte "string".match?(/regex/)le differenze possono essere dovute solo all'attività della macchina.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.