Найшвидший спосіб перевірити, чи рядок відповідає регулярному виразу в ruby?


97

Який найшвидший спосіб перевірити, чи рядок відповідає регулярному виразу в Ruby?

Моя проблема полягає в тому, що мені доводиться "egrep" перелічувати величезний список рядків, щоб знайти, які саме відповідають регулярному вираженню, що надається під час виконання. Мене цікавить лише те, чи рядок відповідає регулярному вираженню, а не те, де він відповідає, і який вміст відповідних груп. Я сподіваюся, це припущення може бути використано для зменшення часу, який мій код витрачає на відповідність регулярним виразам.

Я завантажую регулярний вираз

pattern = Regexp.new(ptx).freeze

Я виявив, що string =~ patternце трохи швидше, ніж string.match(pattern).

Чи існують інші прийоми або комбінації клавіш, які можуть допомогти зробити цей тест ще швидшим?


Якщо ви не дбаєте про вміст відповідних груп, навіщо вони у вас? Ви можете зробити регулярні вирази швидшими, перетворивши їх на не захоплюючі.
Mark Thomas

1
Оскільки регулярний вираз надається під час виконання, я припускаю, що він не обмежений, і в цьому випадку в reg-exp можуть бути внутрішні посилання на групування, і тому перетворення їх у не захоплюючі шляхом зміни регулярного виразу може змінити результат (якщо ви не додатково перевірити наявність внутрішніх посилань, але проблема стає дедалі складнішою). Я вважаю, що цікаво = ~ було б швидше, ніж string.match.
djconnel

яка користь від заморожування регулярного виразу тут?
Хардік

Відповіді:


103

Починаючи з Ruby 2.4.0, ви можете використовувати RegExp#match?:

pattern.match?(string)

Regexp#match?явно вказано як підвищення продуктивності в примітках до випуску для 2.4.0 , оскільки дозволяє уникнути розподілу об'єктів, виконаних іншими методами, такими як Regexp#matchі=~ :

Regexp # збіг?
Додано Regexp#match?, яке виконує збіг регулярного виразу, не створюючи об’єкт зворотного посилання і не змінюючи його $~для зменшення розподілу об’єктів.


5
Дякую за пропозицію. Я оновив базовий сценарій і 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)

Так =~швидше, але це залежить від того, що ви хочете мати як повернене значення. Якщо ви просто хочете перевірити, чи містить текст регулярний вираз чи його не використовувати=~


2
Як я писав, я вже виявив, що =~це швидше, ніж matchз менш значним збільшенням продуктивності при роботі на більших регулярних виразах. Мені цікаво, чи є якийсь дивний спосіб зробити цю перевірку ще швидшим, можливо, використовуючи якийсь дивний метод у Regexp або якусь дивну конструкцію.
gioele

Я думаю, що інших рішень немає
Дугі

Що про !("test123" !~ /1/)?
ma11hew28,

1
@MattDiPasquale, два рази зворотного не повинні бути швидшими, ніж"test123" =~ /1/
Дугі

1
/1/.match?("test123")швидше, ніж "test123" =~ /1/якщо це лише перевірка, чи містить текст регулярний вираз чи ні.
норай

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

Результати МРТ 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)

Результати МРТ 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)

Результати МРТ 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)

Результати МРТ 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(порівняння випадку)?

Оскільки він оцінює істинність чи хибність і не потребує зберігання збігів, повернення індексу збігів та іншої інформації, мені цікаво, чи був би це ще швидшим способом порівняння, ніж =~.


Добре, я тестував це. =~все одно швидше, навіть якщо у вас є кілька груп захоплення, проте це швидше, ніж інші варіанти.

До речі, що хорошого freeze? Я не міг виміряти жодного підвищення продуктивності від цього.


Ефекти freezeне відображатимуться в результатах, оскільки це відбувається до циклів тесту і діє на сам шаблон.
The Tin Man

5

Залежно від того, наскільки складним є ваш регулярний вираз, ви можете просто використовувати просту нарізку рядків. Я не впевнений у практичності цього для вашої програми чи в тому, чи він справді може запропонувати якісь покращення швидкості.

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

Я не можу використовувати нарізання рядків, оскільки регулярний вираз надається під час виконання, і я не маю жодного контролю над цим.
gioele

Ви можете використовувати нарізання рядків, тільки не нарізання за допомогою фіксованого рядка. Використовуйте змінну замість рядка в лапках, і це все одно буде працювати.
Олов'яна людина

3

Мені цікаво, чи є якийсь дивний спосіб зробити цю перевірку ще швидшим, можливо, використовуючи якийсь дивний метод у Regexp або якусь дивну конструкцію.

Двигуни Regexp відрізняються тим, як вони здійснюють пошук, але, загалом, закріплюють ваші шаблони на швидкість і уникають жадібних збігів, особливо при пошуку довгих рядків.

Найкраще, поки ви не ознайомитесь з тим, як працює певний движок, - це робити тести та додавати / видаляти прив’язки, спробувати обмежити пошук, використовувати підстановочні знаки проти явних збігів тощо.

Фруктово камінь дуже корисна для швидкого бенчмаркінг речей, тому що це розумно. Вбудований Benchmark- код Ruby також корисний, хоча ви можете писати тести, які обманюють вас, не дотримуючись обережності.

Я використовував обидва варіанти у багатьох відповідях тут, на Stack Overflow, тому ви можете шукати мої відповіді та побачити безліч маленьких хитрощів та результатів, щоб дати вам ідеї щодо того, як писати швидший код.

Найбільше, що слід пам’ятати, це погано передчасно оптимізувати свій код, перш ніж дізнатись, де відбуваються уповільнення.


0

Щоб заповнити відповіді Wiktor Stribiżew та Dougui, я б сказав, що /regex/.match?("string")приблизно так само швидко "string".match?(/regex/).

Рубін 2.4.0 (10000000 ~ 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> 

Рубін 2.6.2 (100 000 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/), різниця може бути лише внаслідок активності машини.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.