Bir dizenin Ruby'deki bir normal ifade ile eşleşip eşleşmediğini kontrol etmenin en hızlı yolu?


102

Ruby'de bir dizenin normal bir ifadeyle eşleşip eşleşmediğini kontrol etmenin en hızlı yolu nedir?

Benim sorunum, çalışma zamanında verilen bir regexp ile eşleşenleri bulmak için çok sayıda dizge üzerinden "egrep" yapmam gerektiğidir. Yalnızca dizenin normal ifadeyle eşleşip eşleşmediğini, nerede eşleştiğini veya eşleşen grupların içeriğinin ne olduğunu önemsiyorum. Umarım bu varsayım, kodumun normal ifadelerle eşleşen harcadığı süreyi azaltmak için kullanılabilir.

Regexp'i şununla yüklüyorum:

pattern = Regexp.new(ptx).freeze

Bunun string =~ patternbiraz daha hızlı olduğunu buldum string.match(pattern).

Bu testi daha da hızlı hale getirmek için kullanılabilecek başka püf noktaları veya kısayollar var mı?


Eşleşen grupların içeriğini önemsemiyorsanız, neden onlara sahipsiniz? Normal ifadeleri yakalamayan hale dönüştürerek daha hızlı hale getirebilirsiniz.
Mark Thomas

1
Düzenli ifade çalışma zamanında sağlandığından, sınırlandırılmamış olduğunu varsayıyorum; bu durumda, reg-exp içinde gruplamalara dahili referanslar olabilir ve bu nedenle normal ifadeyi değiştirerek bunları yakalamayan hale dönüştürmek sonucu değiştirebilir (siz ek olarak dahili referansları kontrol edin, ancak sorun giderek daha karmaşık hale gelir). Meraklı buluyorum = ~ string.match'ten daha hızlı olurdu.
djconnel

burada regexp'i dondurmanın faydası nedir?
Hardik

Yanıtlar:


111

Ruby 2.4.0'dan başlayarak şunları kullanabilirsiniz RegExp#match?:

pattern.match?(string)

Regexp#match?2.4.0 sürüm notlarında açıkça bir performans iyileştirmesi olarak listelenmiştir, çünkü Regexp#matchve gibi diğer yöntemlerle gerçekleştirilen nesne tahsislerinden kaçınır =~:

Normal ifade # eşleşme? Geri referans nesnesi oluşturmadan ve nesne tahsisini azaltmak için değiştirmeden bir regexp eşleşmesini yürüten
eklendi .Regexp#match?$~


6
Önerin için teşekkürler. Kıyaslama komut dosyasını güncelledim ve Regexp#match?gerçekten de diğer alternatiflerden en az% 50 daha hızlı.
gioele

74

Bu basit bir ölçüt:

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)

Yani =~daha hızlıdır, ancak döndürülen değer olarak neye sahip olmak istediğinize bağlıdır. Metnin bir normal ifade içerip içermediğini kontrol etmek istiyorsanız=~


2
Yazdığım gibi, daha büyük regexps üzerinde çalışırken daha az dramatik bir performans artışı ile bunun daha =~hızlı olduğunu zaten buldum match. Merak ettiğim şey, bu kontrolü daha da hızlı hale getirmenin garip bir yolu olup olmadığı, belki Regexp'deki garip bir yöntemi veya bazı garip yapıları kullanarak.
gioele

Sanırım başka çözüm yok
Dougui

Peki ya !("test123" !~ /1/)?
ma11hew28

1
@MattDiPasquale, iki kat tersinin daha hızlı olmaması"test123" =~ /1/
Dougui

1
/1/.match?("test123")"test123" =~ /1/metnin bir normal ifade içerip içermediğini kontrol etmekten daha hızlıdır .
noraj

42

Bu, internette bazı makaleler bulduktan sonra çalıştırdığım kriter.

2.4.0 ile kazanan re.match?(str)(@ wiktor-stribiżew tarafından önerildiği gibi), önceki sürümlerde, neredeyse aynı hızda re =~ strolmasına rağmen en hızlı görünüyor str =~ re.

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

Sonuçlar MR 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)

Sonuçlar MRG 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)

Sonuçlar MRI 2.3.3 (normal ifade eşleşmesinde bir gerileme var gibi görünüyor):

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

Sonuçlar 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)

Sadece not eklemek için, gerçek formlar bunlardan daha hızlıdır. Örn. /a+b/ =~ strVe str =~ /a+b/. İşlevler aracılığıyla yinelendiğinde bile geçerlidir ve bunun, normal ifadeleri bir değişkende saklamak ve dondurmaktan daha iyi olduğunu düşünmek için yeterince geçerli görüyorum. Komut dosyamı ruby ​​1.9.3p547, ruby ​​2.0.0p481 ve ruby ​​2.1.4p265 ile test ettim. Bu iyileştirmelerin sonraki yamalarda yapılmış olması mümkündür, ancak henüz önceki sürümler / yamalar ile test etme planım yok.
konsolebox

Daha !(re !~ str)hızlı olabileceğini düşündüm , ama değil.
ma11hew28

7

Peki ya re === str(durum karşılaştırması)?

Doğru ya da yanlış olarak değerlendirildiği ve eşleşmeleri saklamaya, eşleşme indeksini döndürmeye ve benzeri şeylere ihtiyacı olmadığından, eşleştirme için daha hızlı bir yol olup olmayacağını merak ediyorum =~.


Tamam, bunu test ettim. =~birden fazla yakalama grubunuz olsa bile daha hızlıdır, ancak diğer seçeneklerden daha hızlıdır.

BTW, ne iyi freeze? Bundan herhangi bir performans artışı ölçemedim.


'İn etkileri freeze, kıyaslama döngülerinden önce meydana geldiği ve kalıbın kendisine etki ettiği için sonuçlarda görünmez.
The Tin Man

5

Normal ifadenizin ne kadar karmaşık olduğuna bağlı olarak, muhtemelen basit dize dilimlemeyi kullanabilirsiniz. Bunun uygulamanız için pratikliğinden veya gerçekten herhangi bir hız iyileştirmesi sunup sunmayacağından emin değilim.

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

Dize dilimlemeyi kullanamıyorum çünkü regexp çalışma zamanında sağlanıyor ve bunun üzerinde herhangi bir kontrolüm yok.
gioele

Sabit bir dize kullanarak dilimlemeyi değil, dize dilimlemeyi kullanabilirsiniz. Tırnak içinde bir dize yerine bir değişken kullanın ve yine de işe yarayacaktır.
The Tin Man

3

Merak ettiğim şey, bu kontrolü daha da hızlı hale getirmenin garip bir yolu olup olmadığı, belki Regexp'deki garip bir yöntemi veya bazı garip yapıları kullanarak.

Regexp motorları, aramaları uygulama şekline göre değişir, ancak genel olarak, hız için kalıplarınızı sabitleyin ve özellikle uzun dizeleri ararken açgözlü eşleşmelerden kaçının.

Belirli bir motorun nasıl çalıştığına aşina olana kadar yapılacak en iyi şey, karşılaştırmalar yapmak ve bağlantı noktaları eklemek / kaldırmak, aramaları sınırlandırmayı denemek, açık eşleşmeler yerine joker karakterler kullanmaktır.

Meyveli zekice çünkü mücevher, hızla kıyaslama şeyler için çok yararlıdır. Ruby'nin yerleşik Benchmark kodu da kullanışlıdır, ancak dikkatli olmadığınız için sizi yanıltacak testler yazabilirsiniz.

Her ikisini de Stack Overflow'da birçok cevapta kullandım, böylece cevaplarımda arama yapabilirsiniz ve size daha hızlı kod yazma konusunda fikir verecek çok sayıda küçük numara ve sonuç göreceksiniz.

Unutulmaması gereken en büyük şey, yavaşlamaların nerede meydana geldiğini bilmeden önce kodunuzu zamanından önce optimize etmenizin kötü olmasıdır.


0

Wiktor Stribiżew ve Dougui cevaplarını tamamlamak için bunu /regex/.match?("string")en hızlı şekilde söyleyebilirim "string".match?(/regex/).

Ruby 2.4.0 (10000000 ~ 2 saniye)

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 saniye)

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>

Not: zamanlar değişir, bazen /regex/.match?("string")daha hızlıdır ve bazen "string".match?(/regex/)farklılıklar yalnızca makine etkinliğinden kaynaklanabilir.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.