Der schnellste Weg, um zu überprüfen, ob eine Zeichenfolge mit einem regulären Ausdruck in Ruby übereinstimmt?

95

Was ist der schnellste Weg, um zu überprüfen, ob eine Zeichenfolge mit einem regulären Ausdruck in Ruby übereinstimmt?

Mein Problem ist, dass ich eine große Liste von Zeichenfolgen "egrep" durchgehen muss, um herauszufinden, welche mit einem regulären Ausdruck übereinstimmen, der zur Laufzeit angegeben wird. Es ist mir nur wichtig, ob die Zeichenfolge mit dem regulären Ausdruck übereinstimmt, nicht, wo sie übereinstimmt oder wie der Inhalt der übereinstimmenden Gruppen ist. Ich hoffe, dass diese Annahme verwendet werden kann, um die Zeit zu reduzieren, die mein Code mit passenden regulären Ausdrücken verbringt.

Ich lade den regulären Ausdruck mit

pattern = Regexp.new(ptx).freeze

Ich habe festgestellt, dass string =~ patterndas etwas schneller ist als string.match(pattern).

Gibt es andere Tricks oder Verknüpfungen, mit denen dieser Test noch schneller durchgeführt werden kann?

gioele
quelle
Wenn Sie sich nicht für den Inhalt der übereinstimmenden Gruppen interessieren, warum haben Sie sie? Sie können den regulären Ausdruck beschleunigen, indem Sie ihn in Nicht-Capture konvertieren.
Mark Thomas
1
Da der reguläre Ausdruck zur Laufzeit bereitgestellt wird, gehe ich davon aus, dass er nicht eingeschränkt ist. In diesem Fall gibt es möglicherweise interne Verweise innerhalb der reg-exp auf Gruppierungen. Wenn Sie sie daher durch Ändern des regulären Ausdrucks in Nichterfassung konvertieren, kann dies das Ergebnis ändern (es sei denn, Sie zusätzlich nach internen Referenzen suchen, aber das Problem wird immer komplexer). Ich finde es merkwürdig = ~ wäre schneller als string.match.
Djconnel
Was ist der Vorteil des Einfrierens des regulären Ausdrucks hier?
Hardik

Antworten:

103

Ab Ruby 2.4.0 können Sie Folgendes verwenden RegExp#match?:

pattern.match?(string)

Regexp#match?wird in den Versionshinweisen für 2.4.0 explizit als Leistungsverbesserung aufgeführt , da Objektzuweisungen vermieden werden, die von anderen Methoden wie Regexp#matchund ausgeführt werden =~.

Regexp # Übereinstimmung?
Hinzugefügt Regexp#match?, wodurch eine reguläre Ausdrucksübereinstimmung ausgeführt wird, ohne dass ein Rückreferenzobjekt erstellt und geändert wird $~, um die Objektzuordnung zu verringern.

Wiktor Stribiżew
quelle
5
Vielen Dank für den Vorschlag. Ich habe das Benchmark-Skript aktualisiert und Regexp#match?ist in der Tat mindestens 50% schneller als die anderen Alternativen.
Gioele
74

Dies ist ein einfacher Maßstab:

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)

Ist =~also schneller, hängt aber davon ab, was Sie als Rückgabewert haben möchten. Wenn Sie nur überprüfen möchten, ob der Text einen regulären Ausdruck enthält oder nicht=~

Dougui
quelle
2
Wie ich schrieb, stellte ich bereits fest, dass dies =~schneller ist als matchbei einer weniger dramatischen Leistungssteigerung, wenn mit größeren regulären Ausdrücken gearbeitet wird. Was ich mich frage, ist, ob es eine seltsame Möglichkeit gibt, diese Überprüfung noch schneller zu machen, vielleicht eine seltsame Methode in Regexp oder ein seltsames Konstrukt auszunutzen.
Gioele
Ich denke, es gibt keine anderen Lösungen
Dougui
Was ist mit !("test123" !~ /1/)?
ma11hew28
1
@ MattDiPasquale, zweimal sollte die Umkehrung nicht schneller sein als"test123" =~ /1/
Dougui
1
/1/.match?("test123")ist schneller als "test123" =~ /1/wenn nur geprüft werden soll, ob der Text einen regulären Ausdruck enthält oder nicht.
Noraj
41

Dies ist der Benchmark, den ich durchgeführt habe, nachdem ich einige Artikel im Internet gefunden habe.

Mit 2.4.0 scheint der Gewinner re.match?(str)(wie von @ wiktor-stribiżew vorgeschlagen) in früheren Versionen re =~ stram schnellsten zu sein, obwohl er str =~ refast genauso schnell ist.

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

Ergebnisse MRT 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)

Ergebnisse MRT 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)

Ergebnisse MRT 2.3.3 (es scheint eine Regression beim Regex-Matching zu geben):

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

Ergebnisse MRT 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
quelle
Nur um die Notiz hinzuzufügen, wörtliche Formen sind schneller als diese. ZB /a+b/ =~ strund str =~ /a+b/. Es ist auch dann gültig, wenn sie durch Funktionen iteriert werden, und ich sehe dies als ausreichend an, um es als besser zu betrachten, als reguläre Ausdrücke in einer Variablen zu speichern und einzufrieren. Ich habe mein Skript mit Ruby 1.9.3p547, Ruby 2.0.0p481 und Ruby 2.1.4p265 getestet. Es ist möglich, dass diese Verbesserungen an späteren Patches vorgenommen wurden, aber ich habe noch nicht vor, sie mit früheren Versionen / Patches zu testen.
konsolebox
Ich dachte, es !(re !~ str)könnte schneller sein, aber es ist nicht so.
ma11hew28
7

Wie wäre es mit re === str (Fallvergleich)?

Da es als wahr oder falsch ausgewertet wird und keine Übereinstimmungen, Rückgabe des Übereinstimmungsindex und dergleichen benötigt werden, frage ich mich, ob dies eine noch schnellere Methode zum Abgleichen wäre als =~.


Ok, ich habe das getestet. =~ist immer noch schneller, selbst wenn Sie mehrere Erfassungsgruppen haben, ist jedoch schneller als die anderen Optionen.

Übrigens, was nützt das freeze? Ich konnte keinen Leistungsschub daran messen.

Heiko
quelle
Die Auswirkungen von freezewerden in den Ergebnissen nicht angezeigt, da sie vor den Benchmark-Schleifen auftreten und auf das Muster selbst einwirken.
der Blechmann
4

Abhängig davon, wie kompliziert Ihr regulärer Ausdruck ist, können Sie möglicherweise einfach das Schneiden von Zeichenfolgen verwenden. Ich bin mir nicht sicher, ob dies für Ihre Anwendung praktikabel ist oder ob es tatsächlich Geschwindigkeitsverbesserungen bietet.

'testsentence'['stsen']
=> 'stsen' # evaluates to true
'testsentence'['koala']
=> nil # evaluates to false
Jimydief
quelle
Ich kann kein String-Slicing verwenden, da der reguläre Ausdruck zur Laufzeit bereitgestellt wird und ich keine Kontrolle darüber habe.
Gioele
Sie können das Schneiden von Zeichenfolgen verwenden, jedoch nicht das Schneiden mit einer festen Zeichenfolge. Verwenden Sie eine Variable anstelle einer Zeichenfolge in Anführungszeichen und es würde immer noch funktionieren.
der Blechmann
3

Was ich mich frage, ist, ob es eine seltsame Möglichkeit gibt, diese Überprüfung noch schneller zu machen, vielleicht eine seltsame Methode in Regexp oder ein seltsames Konstrukt auszunutzen.

Regexp-Engines unterscheiden sich in der Art und Weise, wie Suchvorgänge implementiert werden. Verankern Sie jedoch im Allgemeinen Ihre Muster auf Geschwindigkeit und vermeiden Sie gierige Übereinstimmungen, insbesondere bei der Suche nach langen Zeichenfolgen.

Das Beste, was Sie tun können, bis Sie mit der Funktionsweise einer bestimmten Engine vertraut sind, ist, Benchmarks durchzuführen und Anker hinzuzufügen / zu entfernen, die Suche einzuschränken, Platzhalter im Vergleich zu expliziten Übereinstimmungen zu verwenden usw.

Das fruchtige Juwel ist sehr nützlich, um Dinge schnell zu vergleichen, weil es intelligent ist. Rubys integrierter Benchmark Code ist ebenfalls nützlich, obwohl Sie Tests schreiben können, die Sie täuschen, wenn Sie nicht vorsichtig sind.

Ich habe beide in vielen Antworten hier auf Stack Overflow verwendet, sodass Sie meine Antworten durchsuchen können und viele kleine Tricks und Ergebnisse sehen können, um Ihnen Ideen zu geben, wie Sie schnelleren Code schreiben können.

Das Wichtigste ist, dass Sie Ihren Code nicht vorzeitig optimieren, bevor Sie wissen, wo die Verlangsamungen auftreten.

der Blechmann
quelle
0

Um die Antworten von Wiktor Stribiżew und Dougui zu vervollständigen, würde ich das /regex/.match?("string")ungefähr so ​​schnell sagen wie"string".match?(/regex/) .

Ruby 2.4.0 (10 000 000 ~ 2 Sek.)

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 Sek.)

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>

Hinweis: Die Zeiten variieren, manchmal /regex/.match?("string")sind sie schneller und manchmal "string".match?(/regex/)sind die Unterschiede möglicherweise nur auf die Maschinenaktivität zurückzuführen.

noraj
quelle