วิธีที่เร็วที่สุดในการตรวจสอบว่าสตริงตรงกับ regexp ในทับทิมหรือไม่?


100

วิธีใดที่เร็วที่สุดในการตรวจสอบว่าสตริงตรงกับนิพจน์ทั่วไปใน Ruby หรือไม่

ปัญหาของฉันคือฉันต้อง "egrep" ผ่านรายการสตริงจำนวนมากเพื่อค้นหาว่าสตริงใดที่ตรงกับ regexp ที่กำหนดให้ในรันไทม์ ฉันสนใจแค่ว่าสตริงตรงกับ regexp ไม่ใช่ที่ที่ตรงกันหรือเนื้อหาของกลุ่มที่ตรงกันคืออะไร ฉันหวังว่าสมมติฐานนี้จะสามารถใช้เพื่อลดระยะเวลาที่รหัสของฉันใช้ในการจับคู่ regexps

ฉันโหลด regexp ด้วย

pattern = Regexp.new(ptx).freeze

ฉันพบว่าstring =~ patternเร็วกว่าstring.match(pattern).

มีเทคนิคหรือทางลัดอื่น ๆ ที่สามารถใช้เพื่อทำให้การทดสอบนี้เร็วขึ้นหรือไม่?


หากคุณไม่สนใจเกี่ยวกับเนื้อหาของกลุ่มที่ตรงกันคุณมีไว้ทำไม คุณสามารถทำให้ regex เร็วขึ้นได้โดยการแปลงเป็น non-capture
Mark Thomas

1
เนื่องจาก regexp มีให้ในรันไทม์ฉันคิดว่ามันไม่ถูก จำกัด ซึ่งในกรณีนี้อาจมีการอ้างอิงภายในภายใน reg-exp เป็นการจัดกลุ่มดังนั้นการแปลงเป็น non-capture โดยการแก้ไข regexp สามารถแก้ไขผลลัพธ์ได้ (เว้นแต่คุณจะ ตรวจสอบการอ้างอิงภายในเพิ่มเติมด้วย แต่ปัญหาจะซับซ้อนขึ้นเรื่อย ๆ ) ฉันพบว่ามันอยากรู้อยากเห็น = ~ จะเร็วกว่า string.match
djconnel

การแช่แข็ง regexp ที่นี่มีประโยชน์อย่างไร
Hardik

คำตอบ:


109

เริ่มต้นด้วย Ruby 2.4.0 คุณสามารถใช้RegExp#match?:

pattern.match?(string)

Regexp#match?ถูกระบุไว้อย่างชัดเจนว่าเป็นการปรับปรุงประสิทธิภาพในบันทึกประจำรุ่นสำหรับ 2.4.0เนื่องจากหลีกเลี่ยงการจัดสรรอ็อบเจ็กต์ที่ดำเนินการโดยวิธีการอื่นเช่นRegexp#matchและ=~:

Regexp # ตรงกันไหม
เพิ่มRegexp#match?แล้วซึ่งดำเนินการจับคู่ regexp โดยไม่ต้องสร้างวัตถุอ้างอิงด้านหลังและเปลี่ยน$~เพื่อลดการจัดสรรอ็อบเจ็กต์


6
ขอบคุณสำหรับข้อเสนอแนะ ฉันได้อัปเดตสคริปต์เปรียบเทียบแล้วและRegexp#match?เร็วกว่าทางเลือกอื่น ๆ อย่างน้อย 50%
gioele

74

นี่คือเกณฑ์มาตรฐานง่ายๆ:

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)

ดังนั้น=~จะเร็ว แต่มันขึ้นอยู่กับสิ่งที่คุณต้องการที่จะมีเป็นค่าที่ส่งกลับ หากคุณต้องการตรวจสอบว่าข้อความนั้นมี regex หรือไม่ใช้=~


2
ตามที่ฉันเขียนฉันพบแล้วว่า=~เร็วกว่าmatchด้วยประสิทธิภาพที่เพิ่มขึ้นอย่างมากน้อยลงเมื่อทำงานกับ regexps ที่ใหญ่กว่า สิ่งที่ฉันสงสัยคือมีวิธีแปลก ๆ ที่จะทำให้การตรวจสอบนี้เร็วขึ้นหรือไม่อาจใช้วิธีแปลก ๆ ใน Regexp หรือโครงสร้างแปลก ๆ
gioele

ฉันคิดว่าไม่มีทางแก้ไขอื่นใด
Dougui

เกี่ยวกับอะไร!("test123" !~ /1/)?
ma11hew28

1
@MattDiPasquale สองครั้งที่ผกผันไม่ควรเร็วกว่า"test123" =~ /1/
Dougui

1
/1/.match?("test123")เร็วกว่า"test123" =~ /1/การตรวจสอบว่าข้อความมีนิพจน์ทั่วไปหรือไม่
noraj

42

นี่คือเกณฑ์มาตรฐานที่ฉันใช้หลังจากพบบทความในเน็ต

ด้วย 2.4.0 ผู้ชนะคือre.match?(str)(ตามคำแนะนำของ @ wiktor-stribiżew) ในเวอร์ชันก่อนหน้าre =~ strดูเหมือนว่าจะเร็วที่สุดแม้ว่าจะเร็ว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

ผลลัพธ์ 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)

ผลลัพธ์ 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)

ผลลัพธ์ MRI 2.3.3 (ดูเหมือนว่ามีการถดถอยในการจับคู่นิพจน์ทั่วไป):

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

ผลลัพธ์ 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)

เพียงเพื่อเพิ่มบันทึกย่อรูปแบบตามตัวอักษรจะเร็วกว่าแบบนี้ เช่น/a+b/ =~ strและstr =~ /a+b/. ใช้ได้แม้ว่าจะทำซ้ำผ่านฟังก์ชันและฉันเห็นว่าสิ่งนี้ถูกต้องเพียงพอที่จะพิจารณาว่าดีกว่าการจัดเก็บและตรึงนิพจน์ทั่วไปในตัวแปร ฉันทดสอบสคริปต์ด้วย Ruby 1.9.3p547, Ruby 2.0.0p481 และ Ruby 2.1.4p265 เป็นไปได้ว่าการปรับปรุงเหล่านี้เกิดขึ้นในแพตช์ในภายหลัง แต่ฉันยังไม่มีแผนที่จะทดสอบกับเวอร์ชัน / แพตช์ก่อนหน้านี้
konsolebox

ฉันคิดว่า!(re !~ str)อาจจะเร็วกว่านี้ แต่ก็ไม่ใช่
ma11hew28

7

แล้วre === str(เปรียบเทียบเคส) ล่ะ?

เนื่องจากมันประเมินว่าเป็นจริงหรือเท็จและไม่จำเป็นต้องจัดเก็บการแข่งขันการส่งคืนดัชนีการจับคู่และสิ่งนั้นฉันจึงสงสัยว่ามันจะเป็นวิธีที่เร็วกว่าในการจับคู่=~หรือไม่


ตกลงฉันทดสอบแล้ว =~ยังเร็วกว่าแม้ว่าคุณจะมีกลุ่มการบันทึกหลายกลุ่ม แต่ก็เร็วกว่าตัวเลือกอื่น ๆ

BTW อะไรดีfreeze? ฉันไม่สามารถวัดการเพิ่มประสิทธิภาพใด ๆ จากมันได้


ผลกระทบของfreezeจะไม่ปรากฏในผลลัพธ์เนื่องจากเกิดขึ้นก่อนลูปเกณฑ์มาตรฐานและดำเนินการกับรูปแบบนั้นเอง
The Tin Man

5

ขึ้นอยู่กับว่านิพจน์ทั่วไปของคุณซับซ้อนเพียงใดคุณอาจใช้การแบ่งสตริงแบบธรรมดาก็ได้ ฉันไม่แน่ใจเกี่ยวกับการใช้งานจริงสำหรับแอปพลิเคชันของคุณหรือว่าจะมีการปรับปรุงความเร็วจริงหรือไม่

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

ฉันไม่สามารถใช้การแบ่งส่วนสตริงได้เนื่องจาก regexp มีให้ในเวลาทำงานและฉันไม่สามารถควบคุมสิ่งนั้น
gioele

คุณสามารถใช้การแบ่งส่วนสตริงไม่ใช่การแบ่งส่วนโดยใช้สตริงคงที่ ใช้ตัวแปรแทนสตริงในเครื่องหมายคำพูดและมันก็ยังใช้งานได้
The Tin Man

3

สิ่งที่ฉันสงสัยคือมีวิธีแปลก ๆ ที่จะทำให้การตรวจสอบนี้เร็วขึ้นหรือไม่อาจใช้วิธีแปลก ๆ ใน Regexp หรือโครงสร้างแปลก ๆ

เอ็นจิ้น Regexp แตกต่างกันไปในวิธีที่ใช้การค้นหา แต่โดยทั่วไปแล้วให้ยึดรูปแบบของคุณเพื่อความเร็วและหลีกเลี่ยงการจับคู่แบบโลภโดยเฉพาะอย่างยิ่งเมื่อค้นหาสตริงที่ยาว

สิ่งที่ดีที่สุดที่ต้องทำจนกว่าคุณจะคุ้นเคยกับวิธีการทำงานของเอ็นจิ้นโดยเฉพาะคือทำการวัดประสิทธิภาพและเพิ่ม / ลบจุดยึดลอง จำกัด การค้นหาใช้สัญลักษณ์แทนกับการจับคู่แบบชัดแจ้ง ฯลฯ

ฟรุ๊ตตี้อัญมณีมีประโยชน์มากสำหรับการเปรียบเทียบได้อย่างรวดเร็วสิ่งเพราะมันเป็นสมาร์ท โค้ดBenchmarkในตัวของ Ruby ยังมีประโยชน์เช่นกันแม้ว่าคุณจะสามารถเขียนการทดสอบที่หลอกคุณได้โดยไม่ระวัง

ฉันใช้ทั้งสองคำตอบใน Stack Overflow แล้วดังนั้นคุณสามารถค้นหาคำตอบของฉันและจะเห็นเทคนิคและผลลัพธ์เล็ก ๆ น้อย ๆ มากมายเพื่อให้คุณมีแนวคิดในการเขียนโค้ดที่เร็วขึ้น

สิ่งที่สำคัญที่สุดที่ต้องจำไว้คือการเพิ่มประสิทธิภาพโค้ดของคุณก่อนเวลาอันควรเป็นเรื่องไม่ดีก่อนที่คุณจะรู้ว่าการชะลอตัวเกิดขึ้นที่ใด


0

เพื่อให้คำตอบWiktor StribiżewและDougui สมบูรณ์ฉันจะบอกว่า/regex/.match?("string")เร็วพอ"string".match?(/regex/)

Ruby 2.4.0 (10,000,000 ~ 2 วินาที)

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 (100000,000 ~ 20 วินาที)

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>

หมายเหตุ: เวลาแตกต่างกันไปบางครั้ง/regex/.match?("string")เร็วกว่าและบางครั้ง"string".match?(/regex/)ความแตกต่างอาจเกิดจากกิจกรรมของเครื่องเท่านั้น

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.