Cara tercepat untuk memeriksa apakah sebuah string cocok dengan ekspresi reguler di ruby?

101

Apa cara tercepat untuk memeriksa apakah sebuah string cocok dengan ekspresi reguler di Ruby?

Masalah saya adalah bahwa saya harus "egrep" melalui daftar besar string untuk menemukan mana yang cocok dengan regexp yang diberikan saat runtime. Saya hanya peduli apakah string cocok dengan regexp, bukan di mana cocok, atau apa konten dari grup yang cocok. Saya berharap asumsi ini dapat digunakan untuk mengurangi jumlah waktu yang dihabiskan kode saya untuk mencocokkan regexps.

Saya memuat regexp dengan

pattern = Regexp.new(ptx).freeze

Saya telah menemukan bahwa string =~ patternini sedikit lebih cepat daripada string.match(pattern).

Apakah ada trik atau pintasan lain yang dapat digunakan untuk membuat pengujian ini lebih cepat?

gioele
sumber
Jika Anda tidak peduli dengan konten dari kelompok yang cocok, mengapa Anda memilikinya? Anda dapat membuat regex lebih cepat dengan mengonversinya menjadi non-capturing.
Mark Thomas
1
Karena regexp disediakan saat run-time, saya menganggap itu tidak dibatasi, dalam hal ini mungkin ada referensi internal dalam reg-exp ke pengelompokan, dan oleh karena itu mengubahnya menjadi non-capturing dengan memodifikasi regexp dapat mengubah hasilnya (kecuali Anda Selain itu, periksa referensi internal, tetapi masalahnya menjadi semakin kompleks). Saya merasa penasaran = ~ akan lebih cepat dari string.match.
djconnel
apa manfaat membekukan regexp di sini?
Hardik

Jawaban:

110

Dimulai dengan Ruby 2.4.0, Anda dapat menggunakan RegExp#match?:

pattern.match?(string)

Regexp#match?secara eksplisit dicantumkan sebagai peningkatan kinerja dalam catatan rilis untuk 2.4.0 , karena ini menghindari alokasi objek yang dilakukan oleh metode lain seperti Regexp#matchdan =~:

Ekspresi reguler # cocok?
Ditambahkan Regexp#match?, yang mengeksekusi pencocokan ekspresi reguler tanpa membuat objek referensi kembali dan mengubah $~untuk mengurangi alokasi objek.

Wiktor Stribiżew
sumber
6
Terima kasih atas sarannya. Saya telah mengupdate script benchmark dan Regexp#match?memang setidaknya 50% lebih cepat dari alternatif lainnya.
gioele
74

Ini adalah patokan sederhana:

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)

Jadi =~lebih cepat tetapi tergantung apa yang ingin Anda miliki sebagai nilai yang dikembalikan. Jika Anda hanya ingin memeriksa apakah teks tersebut mengandung regex atau tidak digunakan=~

Dougui
sumber
2
Saat saya menulis, saya sudah menemukan bahwa =~lebih cepat daripada match, dengan peningkatan kinerja yang tidak terlalu dramatis saat beroperasi pada regex yang lebih besar. Apa yang saya ingin tahu adalah apakah ada cara aneh untuk membuat pemeriksaan ini lebih cepat, mungkin mengeksploitasi beberapa metode aneh di Regexp atau beberapa konstruksi aneh.
gioele
Saya pikir tidak ada solusi lain
Dougui
Tentang apa !("test123" !~ /1/)?
ma11hew28
1
@MattDiPasquale, dua kali pembalikan seharusnya tidak lebih cepat dari"test123" =~ /1/
Dougui
1
/1/.match?("test123")lebih cepat daripada "test123" =~ /1/jika hanya untuk memeriksa apakah teks tersebut mengandung regex atau tidak.
noraj
42

Ini adalah patokan yang saya jalankan setelah menemukan beberapa artikel di internet.

Dengan 2.4.0, pemenangnya adalah re.match?(str)(seperti yang disarankan oleh @ wiktor-stribiżew), pada versi sebelumnya, re =~ strtampaknya menjadi yang tercepat, meskipun str =~ rehampir sama cepatnya.

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

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

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

Hasil MRI 2.3.3 (tampaknya ada regresi dalam pencocokan regex):

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

Hasil 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
sumber
Sekadar menambahkan catatan, bentuk literal lebih cepat dari ini. Misalnya /a+b/ =~ strdan str =~ /a+b/. Ini valid bahkan ketika mengulanginya melalui fungsi dan saya melihat ini cukup valid untuk dianggap lebih baik daripada menyimpan dan membekukan ekspresi reguler pada variabel. Saya menguji skrip saya dengan ruby ​​1.9.3p547, ruby ​​2.0.0p481 dan ruby ​​2.1.4p265. Mungkin saja peningkatan ini dilakukan pada tambalan selanjutnya tetapi saya belum berencana untuk mengujinya dengan versi / tambalan sebelumnya.
konsolebox
Saya pikir !(re !~ str)mungkin lebih cepat, tetapi ternyata tidak.
ma11hew28
7

Bagaimana dengan re === str(perbandingan kasus)?

Karena itu mengevaluasi benar atau salah dan tidak perlu menyimpan kecocokan, mengembalikan indeks kecocokan dan hal-hal itu, saya bertanya-tanya apakah itu akan menjadi cara yang lebih cepat untuk mencocokkan daripada =~.


Oke, saya menguji ini. =~masih lebih cepat, bahkan jika Anda memiliki beberapa grup penangkapan, namun ini lebih cepat daripada opsi lainnya.

BTW, apa gunanya freeze? Saya tidak dapat mengukur peningkatan kinerja apa pun darinya.

Heiko
sumber
Efek dari freezetidak akan muncul dalam hasil karena terjadi sebelum tolok ukur loop, dan bekerja pada pola itu sendiri.
the Tin Man
5

Bergantung pada seberapa rumit ekspresi reguler Anda, Anda mungkin dapat menggunakan pemotongan string sederhana. Saya tidak yakin tentang kepraktisan ini untuk aplikasi Anda atau apakah itu benar-benar menawarkan peningkatan kecepatan atau tidak.

'testsentence'['stsen']
=> 'stsen' # evaluates to true
'testsentence'['koala']
=> nil # evaluates to false
jimmydief
sumber
Saya tidak dapat menggunakan pemotongan string karena regexp disediakan saat run-time dan saya tidak memiliki kendali atas hal itu.
gioele
Anda dapat menggunakan pemotongan string, hanya saja tidak mengiris menggunakan string tetap. Gunakan variabel sebagai pengganti string dalam tanda kutip dan itu akan tetap berfungsi.
Tin Man
3

Apa yang saya ingin tahu adalah apakah ada cara aneh untuk membuat pemeriksaan ini lebih cepat, mungkin mengeksploitasi beberapa metode aneh di Regexp atau beberapa konstruksi aneh.

Mesin Regexp bervariasi dalam cara mereka mengimplementasikan pencarian, tetapi, secara umum, jangkar pola Anda untuk kecepatan, dan hindari kecocokan serakah, terutama saat mencari string yang panjang.

Hal terbaik untuk dilakukan, sampai Anda terbiasa dengan cara kerja mesin tertentu, adalah melakukan tolok ukur dan menambah / menghapus jangkar, mencoba membatasi pencarian, menggunakan karakter pengganti vs. pencocokan eksplisit, dll.

The Fruity permata ini sangat berguna untuk hal-hal dengan cepat benchmarking, karena itu cerdas. Kode Benchmark bawaan Ruby juga berguna, meskipun Anda dapat menulis tes yang menipu Anda dengan tidak berhati-hati.

Saya telah menggunakan keduanya dalam banyak jawaban di sini di Stack Overflow, sehingga Anda dapat menelusuri jawaban saya dan akan melihat banyak trik dan hasil kecil untuk memberi Anda ide tentang cara menulis kode yang lebih cepat.

Hal terbesar yang perlu diingat adalah, buruk untuk mengoptimalkan kode sebelum waktunya sebelum Anda tahu di mana pelambatan terjadi.

Manusia Timah
sumber
0

Untuk melengkapi jawaban Wiktor Stribiżew dan Dougui, saya akan mengatakannya /regex/.match?("string")secepat itu "string".match?(/regex/).

Ruby 2.4.0 (10.000.000 ~ 2 detik)

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

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>

Catatan: waktu bervariasi, terkadang /regex/.match?("string")lebih cepat dan terkadang "string".match?(/regex/), perbedaan tersebut mungkin hanya karena aktivitas mesin.

noraj
sumber