¿La forma más rápida de verificar si una cadena coincide con una expresión regular en ruby?

97

¿Cuál es la forma más rápida de comprobar si una cadena coincide con una expresión regular en Ruby?

Mi problema es que tengo que "egrep" a través de una enorme lista de cadenas para encontrar cuáles son las que coinciden con una expresión regular que se proporciona en tiempo de ejecución. Solo me importa si la cadena coincide con la expresión regular, no dónde coincide ni cuál es el contenido de los grupos coincidentes. Espero que esta suposición se pueda utilizar para reducir la cantidad de tiempo que mi código pasa haciendo coincidir expresiones regulares.

Cargo la expresión regular con

pattern = Regexp.new(ptx).freeze

He descubierto que string =~ patternes un poco más rápido que string.match(pattern).

¿Hay otros trucos o atajos que puedan usarse para hacer esta prueba aún más rápida?

gioele
fuente
Si no le importa el contenido de los grupos coincidentes, ¿por qué los tiene? Puede hacer que las expresiones regulares sean más rápidas convirtiéndolas en no capturadoras.
Mark Thomas
1
Dado que la expresión regular se proporciona en tiempo de ejecución, supongo que no está restringida, en cuyo caso puede haber referencias internas dentro de la expresión regular a agrupaciones y, por lo tanto, convertirlas en no capturadoras modificando la expresión regular podría modificar el resultado (a menos que usted compruebe además las referencias internas, pero el problema se vuelve cada vez más complejo). Me parece curioso = ~ sería más rápido que string.match.
djconnel
¿Cuál es el beneficio de congelar la expresión regular aquí?
Hardik

Respuestas:

103

A partir de Ruby 2.4.0, puede usar RegExp#match?:

pattern.match?(string)

Regexp#match?se enumera explícitamente como una mejora de rendimiento en las notas de la versión para 2.4.0 , ya que evita las asignaciones de objetos realizadas por otros métodos como Regexp#matchy =~:

¿Regexp # coincidencia?
Agregado Regexp#match?, que ejecuta una coincidencia de expresiones regulares sin crear un objeto de referencia posterior y sin cambiar $~para reducir la asignación de objetos.

Wiktor Stribiżew
fuente
5
Gracias por la sugerencia. He actualizado el script de referencia y, de Regexp#match?hecho, es al menos un 50% más rápido que las otras alternativas.
gioele
74

Este es un punto de referencia simple:

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)

Entonces =~es más rápido, pero depende de lo que desee tener como valor devuelto. Si solo desea verificar si el texto contiene una expresión regular o no usar=~

Dougui
fuente
2
Como escribí, ya descubrí que =~es más rápido que match, con un aumento de rendimiento menos dramático cuando se opera en expresiones regulares más grandes. Lo que me pregunto es si hay alguna forma extraña de hacer que esta verificación sea aún más rápida, tal vez explotando algún método extraño en Regexp o alguna construcción extraña.
gioele
Creo que no hay otras soluciones
Dougui
¿Qué hay de !("test123" !~ /1/)?
ma11hew28
1
@MattDiPasquale, dos veces lo inverso no debería ser más rápido que"test123" =~ /1/
Dougui
1
/1/.match?("test123")es más rápido que "test123" =~ /1/si es solo para verificar si el texto contiene una expresión regular o no.
noraj
42

Este es el punto de referencia que he realizado después de encontrar algunos artículos en la red.

Con 2.4.0 el ganador es re.match?(str)(como sugiere @ wiktor-stribiżew), en versiones anteriores, re =~ strparece ser el más rápido, aunque str =~ rees casi tan rápido.

#!/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

Resultados RM 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)

Resultados RM 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)

Resultados MRI 2.3.3 (parece que hay una regresión en la coincidencia de expresiones regulares):

$ ./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)

Resultados 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)
gioele
fuente
Solo para agregar una nota, las formas literales son más rápidas que estas. Por ejemplo, /a+b/ =~ stry str =~ /a+b/. Es válido incluso cuando se iteran a través de funciones y lo veo lo suficientemente válido como para considerarlo mejor que almacenar y congelar expresiones regulares en una variable. Probé mi script con ruby ​​1.9.3p547, ruby ​​2.0.0p481 y ruby ​​2.1.4p265. Es posible que estas mejoras se hayan realizado en parches posteriores, pero todavía no tengo planes de probarlo con versiones / parches anteriores.
konsolebox
Pensé que !(re !~ str)podría ser más rápido, pero no lo es.
ma11hew28
7

¿Qué pasa con re === str(comparación de casos)?

Dado que se evalúa como verdadero o falso y no tiene necesidad de almacenar coincidencias, devolver el índice de coincidencia y esas cosas, me pregunto si sería una forma aún más rápida de hacer coincidencias que =~.


Ok, probé esto. =~es aún más rápido, incluso si tiene varios grupos de captura, sin embargo, es más rápido que las otras opciones.

Por cierto, ¿de qué sirve freeze? No pude medir ningún aumento de rendimiento de eso.

Heiko
fuente
Los efectos de freezeno aparecerán en los resultados porque ocurre antes de los bucles de referencia y actúa sobre el patrón en sí.
The Tin Man
5

Dependiendo de lo complicada que sea su expresión regular, posiblemente podría usar un simple corte de cadena. No estoy seguro de la practicidad de esto para su aplicación o si realmente ofrecería alguna mejora de velocidad.

'testsentence'['stsen']
=> 'stsen' # evaluates to true
'testsentence'['koala']
=> nil # evaluates to false
jimmydief
fuente
No puedo usar el corte de cadenas porque la expresión regular se proporciona en tiempo de ejecución y no tengo ningún control sobre eso.
gioele
Puede utilizar el corte de cuerda, pero no el uso de una cuerda fija. Use una variable en lugar de una cadena entre comillas y aún funcionará.
The Tin Man
3

Lo que me pregunto es si hay alguna forma extraña de hacer que esta verificación sea aún más rápida, tal vez explotando algún método extraño en Regexp o alguna construcción extraña.

Los motores de expresiones regulares varían en la forma en que implementan las búsquedas, pero, en general, anclan sus patrones de velocidad y evitan coincidencias codiciosas, especialmente cuando se buscan cadenas largas.

Lo mejor que puede hacer, hasta que esté familiarizado con el funcionamiento de un motor en particular, es hacer evaluaciones comparativas y agregar / eliminar anclajes, intentar limitar las búsquedas, usar comodines frente a coincidencias explícitas, etc.

La gema frutal es muy útil para evaluar rápidamente las cosas, porque es inteligente. El código Benchmark incorporado de Ruby también es útil, aunque puede escribir pruebas que lo engañen si no tiene cuidado.

He usado ambos en muchas respuestas aquí en Stack Overflow, por lo que puede buscar entre mis respuestas y verá muchos pequeños trucos y resultados para darle ideas sobre cómo escribir código más rápido.

Lo más importante para recordar es que es malo optimizar prematuramente su código antes de saber dónde ocurren las ralentizaciones.

el hombre de hojalata
fuente
0

Para completar las respuestas de Wiktor Stribiżew y Dougui , diría eso /regex/.match?("string")tan rápido como "string".match?(/regex/).

Ruby 2.4.0 (10000000 ~ 2 segundos)

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 (100 000 000 ~ 20 segundos)

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: los tiempos varían, a veces /regex/.match?("string")es más rápido y "string".match?(/regex/), a veces , las diferencias pueden deberse solo a la actividad de la máquina.

Noraj
fuente