• Strona główna
  • Curriculum Vitae
  • O mnie
  • Mapa strony
  • Kontakt
Niebieski Pomarańczowy Zielony Różowy Fioletowy

Optymalizacja wyrażeń regularnych

Opublikowane 24 sierpnia 2010. Autor: Kamil Brenk. Wizyt: 1 387.

Kategorie: Wyrażenia regularne
Tematyka: optymalizacja serwisów, programowane ciekawostki, wydajność serwisów internetowych, Wyrażenia regularne

sie 24

Temat, do którego podjęcia zabierałem się już jakiś czas. Nie jest to łatwy temat, więc nie było zbyt wielu chęci ku wykonaniu testów sprawdzających wydajność niektórych popularnych sposobów pisania wyrażeń regularnych. W końcu się zabrałem i prezentuję sposoby optymalizacji wyrażeń regularnych.

Nie będę tutaj jednak opisywał podstaw wyrażeń regularnych, nie będę pisał o matematycznej ich analizie ani zagłębiał się w „wewnętrzne” procesy interpretera wyrażeń regularnych. Zajmiemy się wyłącznie wydajnością wyrażeń regularnych!

Na ten temat napisano już wiele – jakby nie patrzeć, to nawet kilka książek :-)

W swoich pomiarach starałem używać się możliwie prostych wyrażeń, tak aby skupić się jedynie na testowanych funkcjonalnościach.

Poniżej prezentuję kilka popularnych sposobów optymalizacji wyrażeń regularnych wraz z benchmarkami i krótkim objaśnieniem.

Benchmark

Do wykonywania pomiarów wykorzystam wcześniej już prezentowaną funkcję Benchmark:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Benchmark($function, $iterations=1000, $args=null) {
    set_time_limit(0);

    if (is_callable($function) === true) {
        $result = microtime(true);

        for ($i = 1; $i <= $iterations; $i++) {
            call_user_func_array($function, $args);
        }

        return round(microtime(true) - $result, 8);
    }

    return false;
}
Grupowanie

Grupowanie polega na oznaczaniu jakiegoś wybranego fragmentu wyrażenia parą nawiasów, tak aby móc przechwycić ten tekst i dalej wykorzystać (co jest bardzo często przydatne), np. :

1
2
3
4
5
$subject = 'http://testy-zawodowe.pl';
$pattern = '/https?:\/\/.*\.([a-zA-Z]{2,5})\/?/';

preg_match($pattern, $subject, $matches);
echo $matches[1];

W ten sposób udało się nam przechwycić domenę najwyższego poziomu, co pozwala na dalsze modyfikacje (np. dodanie flagi narodowej).

Nie o to jednak tutaj chodzi. Bardzo często trafiam na przykłady, w których jest stosowane grupowanie wyrażeń, choć później przechwycone w ten sposób fragmenty nie są wykorzystywane.

Poniżej prezentuję wyniki stosowania optymalizacji dla tego typu wyrażeń.

#1 – mała ilość danych wejściowych

Tekst trafiający do wyrażenia:

1
http://testy-zawodowe.pl/

Testowane wyrażenie z niepotrzebnym grupowaniem:

1
/(http:\/\/)(testy-zawodowe)\.(pl)/

Testowane wyrażenie bez grupowania:

1
/http:\/\/testy-zawodowe\.pl/

Wyniki (czas wyrażony w sekundach):

Metoda / Iteracje1001000100001000001000000
z grupowaniem0.00070.00660.06650.74646.6252
bez grupowania0.00060.00580.05870.57245.7640

#2 – duża ilość danych wejściowych

Tekst trafiający do wyrażenia:
http://blog.kamilbrenk.pl/files/precle.txt

Testowane wyrażenie z niepotrzebnym grupowaniem:

1
/(http:\/\/)(.*)\.([a-z][a-z])/

Testowane wyrażenie bez grupowania:

1
/http:\/\/.*\.[a-z][a-z]/

Wyniki (czas wyrażony w sekundach):

Metoda / Iteracje110100100010000
z grupowaniem0.00390.03790.40194.11941.4623
bez grupowania0.00200.01980.20932.15523.5625

Wyrażenie to przechodzi przez powyższą listę adresów internetowych i wybiera tylko te, które zaczynają się od http:// i kończą dwuliterową domeną (z tego co zauważyłem to domeny narodowe mają dwa znaki, być może z jakimiś wyjątkami).

Jak widać, w przypadku dużych ilości danych jest duża różnica w wydajności poszczególnych rozwiązań. Warto mieć to na uwadze :-)

Przechwytywanie

Idąc za ciosem, należy przetestować i przechwytywanie, które pozwala zapamiętać wybrane fragmenty tekstu poddane wyrażeniu.

Jak już wyżej wspomniałem, nie zawsze jest potrzebne takie zapamiętywanie – często niepotrzebnie zabiera to tylko cenne zasoby procesora.

W takim razie należy stosować składnię nie pozwalającą na przechwytywanie (mowa o operatorze ?: umieszczonym po lewym nawiasie).

#1 – mała ilość danych wejściowych

Tekst trafiający do wyrażenia:

1
http://testy-zawodowe.pl/

Testowane wyrażenie z przechwytywaniem:

1
/(http:\/\/)(testy-zawodowe)\.(pl)/

Testowane wyrażenie bez przechwytywania:

1
/(?:http:\/\/)(?:testy-zawodowe)\.(?:pl)/

Wyniki (czas wyrażony w sekundach):

Metoda / Iteracje1001000100001000001000000
z przechwytywaniem0.00170.01630.14270.71086.9077
bez przechwytywania0.00150.01510.06410.63716.4058

#2 – duża ilość danych wejściowych

Tekst trafiający do wyrażenia:
http://blog.kamilbrenk.pl/files/precle.txt

Testowane wyrażenie z przechwytywaniem:

1
/(http:\/\/)(testy-zawodowe)\.(pl)/

Testowane wyrażenie bez przechwytywania:

1
/(?:http:\/\/)(?:testy-zawodowe)\.(?:pl)/

Wyniki (czas wyrażony w sekundach):

Metoda / Iteracje110100100010000
z przechwytywaniem0.00490.05430.55245.708867.2913
bez przechwytywania0.00280.03000.31523.236533.4211

I tutaj wyraźnie widać, że przy większych ilościach danych różnice są bardzo wyraźne.

Kwantyfikatory zachłanne

Bardzo często trafiam na nadmierne wykorzystywanie kwantyfikatorów zachłannych w miejscach, gdzie wystarczyłoby zwykłe określenie pożądanych wartości ciągu trafiającego pod wyrażenie.

Mówiąc jaśniej, chcemy przeanalizować wprowadzony kod pocztowy. Kod pocztowy ma następujący format: XX-XXX, gdzie X to liczba od 0 do 9. Przykład wyrażenia z niepotrzebnym wykorzystaniem zachłanności:

1
/[[:digit:]]+-[[:digit:]]+/

Dlaczego to wyrażenie jest złe? Ano bardziej wydajnie można napisać to bez użycia zachłanności:

1
/[[:digit:]][[:digit:]]-[[:digit:]][[:digit:]][[:digit:]]+/

Poniżej prezentuję benchmark dla wyrażeń zawierających operatory zachłanne i wyrażeń niewykorzystujących zachłanności.

#1 – mała ilość danych wejściowych

Tekst trafiający do wyrażenia:

1
http://testy-zawodowe.pl

Testowane wyrażenie z operatorami zachłannymi:

1
/.*:\/\/www\.testy-zawodowe\..*/

Testowane wyrażenie bez operatorów zachłannych:

1
/http:\/\/www\.testy-zawodowe\.pl/

Wyniki (czas wyrażony w sekundach):

Metoda / iteracje1001000100001000001000000
zachłanne0.00270.02690.17881.150711.4565
niezachłanne0.00130.01380.05720.56775.7788

#2 – duża ilość danych wejściowych

Tekst trafiający do wyrażenia:
http://blog.kamilbrenk.pl/files/precle.txt

Testowane wyrażenie z operatorami zachłannymi:

1
/.*:\/\//

Testowane wyrażenie bez operatorów zachłannych:

1
/http:\/\//

Wyniki (czas wyrażony w sekundach):

Metoda / iteracje110100100010000
zachłanne0.01420.14681.485614.4443143.4883
niezachłanne0.00120.00940.12141.081110.9888

W przypadku obu wyrażeń pobieramy wszystkie adresy zaczynające się od HTTP. W powyższej liście adresów wszystkie adresy zaczynają się w ten sposób, myślę więc, że próba jest całkiem dobrze przedstawiona (zwracany jest ten sam wynik).

Jak widać, kwantyfikatory zachłanne spowalniają wykonywanie wyrażeń, czasem nawet wielokrotnie dłużej (w zależności od złożoności wzorca).

Przykładowo, bardzo często widzę stosowanie niniejszego wzorca:

1
^(.*)$

Co w tym złego? Ano wzorzec ten wymusza na mechanizmie wyrażenia regularnego przeanalizowanie całego ciągu tekstowego i porównanie każdego znaku z symbolem wieloznaczności (. – kropka) w celu sprawdzenia, czy znak pasuje do wzorca. Oczywiście każdy znak pasuje do tego wzorca – jednak ciąg o n znakach będzie powodował wykonanie n operacji porównania.

Kilka wystąpień tego samego ciągu

Kolejny przykład dotyczy optymalizacji wyrażeń, w których musimy analizować ciągi zawierające kilka tych samych znaków.

Przykładem takiego ciągu jest numer telefonu, np. „tel. 609234934″. Jak najwydajniej przeanalizować taki ciąg? Do wyboru mamy kilka możliwości.

#1 – mała ilość danych wejściowych

Tekst trafiający do wyrażenia:

1
693988422

Z wykorzystaniem kwantyfikatorów zachłannych (zachłanne-1):

1
/[0-9]+/

Z wykorzystaniem kwantyfikatorów zachłannych, określając liczbę wystąpień ciągu (zachłanne-2):

1
/[0-9]{9}/

Oraz przypadek bez wykorzystania zachłanności (niezachłanne):

1
/[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]/

Wyniki (czas wyrażony w sekundach):

Metoda / iteracje1001000100001000001000000
zachłanne-10.00070.00690.06870.69306.8986
zachłanne-20.00060.00600.06060.59565.9913
niezachłanne0.00070.00640.06470.65886.3716

#2 – duża ilość danych wejściowych

Tekst trafiający do wyrażenia:
http://blog.kamilbrenk.pl/files/numerki.txt (2.000 losowych 9-cyfrowych numerów)

Z wykorzystaniem kwantyfikatorów zachłannych (zachłanne-1):

1
/[0-9]+\n/

Z wykorzystaniem kwantyfikatorów zachłannych, określając liczbę wystąpień ciągu (zachłanne-2):

1
/[0-9]{9}\n/

Oraz przypadek bez wykorzystania zachłanności (niezachłanne):

1
/[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]\n/

Wyniki (czas wyrażony w sekundach):

Metoda / iteracje110100100010000
zachłanne-10.00610.06220.60176.070360.9530
zachłanne-20.00510.04920.49064.868548.7464
niezachłanne0.00570.05450.59075.384853.8382

Jak widać po wynikach testu, najbardziej wydajnym rozwiązaniem jest zastosowanie kwantyfikatora zachłannego określającego ilość wystąpień powtórzeń.

Najgorszym natomiast rozwiązaniem jest zastosowanie zachłanności „nieograniczonej” (mowa tutaj o operatorze * oraz +).

W niektórych przypadkach najlepszym jednak rozwiązaniem jest samodzielne wypisanie ilości powtórzeń danego ciągu (by nie zaciągać do tego operatorów zachłannych, które czasami potrafią nieźle obciążyć maszynę, na której są wykonywane).

Modyfikatory

Kolejny test dotyczyć będzie wykorzystania modyfikatora i, który sprawia iż wielkość liter jest ignorowana w ciągu trafiającym do wyrażenia.

Lepiej jest stosować ten modyfikator czy samodzielnie wypisywać wielkie litery w przedziałach, gdzie będzie to konieczne? Przekonajmy się!

#1 – mała ilość danych wejściowych

Tekst trafiający do wyrażenia:

1
PacMan

Testowane wyrażenie z modyfikatorem:

1
/[a-z]{6}/i

Testowane wyrażenie bez modyfikatora:

1
/[a-zA-Z]{6}/

Wyniki (czas wyrażony w sekundach):

Metoda / iteracje1001000100001000001000000
z modyfikatorem0.00130.01310.12681.285012.884
bez modyfikatora0.00150.01310.13111.354113.2853

#2 – duża ilość danych wejściowych

Tekst trafiający do wyrażenia:
Hypertext Transfer Protocol — HTTP/1.0

Testowane wyrażenie z modyfikatorem:

1
/http/i

Testowane wyrażenie bez modyfikatora:

1
/[Hh][Tt][Tt][Pp]/

Wyniki (czas wyrażony w sekundach):

Metoda / iteracje110100100010000
z modyfikatorem0.00460.01770.17171.729718.1458
bez modyfikatora0.08690.39603.665536.6532371.6014

Jak widać, jest tutaj niemała różnica w wydajności poszczególnych rozwiązań. I co najdziwniejsze, wyrażenie z modyfikatorem wykonuje się kilkanaście razy szybciej niż bez modyfikatora!

Jest to o tyle dziwne, że czytając kiedyś książkę o wyrażeniach regularnych autor sugerował, by jak najmniej używać modyfikatora /i. Niestety powyższy test nie potwierdza tych teorii (a wykonałem kilkanaście różnych testów, gdyż byłem przekonany, że to ja robię źle testy). Czy może faktycznie robię coś źle?

Podsumowanie

Jak można zobaczyć na powyższych benchmarkach, są to w większości przypadków mikro-optymalizacje mające niewielki wpływ na działanie aplikacji.

Niemniej jednak czasami musimy przeprowadzić wiele testów i walidacji z użyciem wyrażeń regularnych po stronie serwera dla każdego użytkownika, co już znacząco może wpłynąć na wykorzystanie procesora. Innym razem musimy wykonywać testy po stronie klienta po każdej zmianie wartości w aktywnym polu. Dobrze więc, aby tworzone przez nas wyrażenia były optymalne i działały bez zarzutów!

Aby spełnić te wymagania, pamiętaj by zawsze testować różne rozwiązania oraz pisać możliwie optymalne rozwiązania (choć czasem przejrzystość i prostota rozwiązania jest ważniejsza niż wydajność).

Pozostaje mi życzyć powodzenia przy pisaniu własnych wyrażeniach regularnych! Dziękuję także za ewentualne uwagi i porady od osób mających większe doświadczenie w temacie optymalizacji wyrażeń.

Podobne wpisy

  • Pobieranie adresów URL z innej strony
  • Konwersja JS i CSS do PNG
  • Jak pobierać zewnętrzne zasoby?
  • Kompresja JavaScript
  • Kompresja CSS

Komentarze (11)

  1. Michal Wachowski 25 sierpnia 2010

    Jak byś mógł zrobić ten sam test ale na dłuższym tekście. Ciekawi mnie różnica wydajności miedzy kilkoma słowami a artykułem.

  2. Kamil Brenk 26 sierpnia 2010

    Zrobiłem testy dla większych ilości danych wejściowych, teraz lepiej widać efekty poszczególnych rozwiązań :-)

  3. eN. 26 sierpnia 2010

    Cóż trochę nieprzemyślane te testy, żeby nie powiedzieć absurdalne, porównywać można dwie rzeczy które oferują taką samą funkcjonalność, cztery pierwsze testy są do niczego nie przydatne. Natomiast piąty porównuje dwa sposoby realizacji tego samego i pokazuje który jest lepszy, całe szczęście doczytałem do końca.

  4. Kamil Brenk 26 sierpnia 2010

    @eN: wszystkie testy zwracają te same dane, tylko realizują to w odmienny sposób (tak jak pisałem, często trafiam na wyrażenia z niepotrzebnymi nawiasami grupującymi czy niepotrzebną zachłannością), stąd takie porównania :-)

    A może mógłbyś zasugerować testy, które lepiej i realniej obrazowałyby wydajność poszczególnych rozwiązań?

  5. Michal Wachowski 26 sierpnia 2010

    hehe, a teraz dodaj kolejne testy dla ereg :D co prawda znika w 5.3 – ale :D

    1
    /[Hh][Tt][Tt][Pp]/

    Toto można zapisać w jeszcze dziwniejszy sposób

    1
    /(H|t)(T|t)(T|t)(P|p)/ :)

    a chyba najlepiej zapisał byś tak

    1
    /[Hh][Tt]{2}[Pp]/

    Tak czy siak – wyrażenia regularne to spory kawał teorii i praktyki do przerobienia by ogarnąć to na przyzwoitym poziomie.

    Co do stosowania modyfikatorów (u mnie to się zwało przełącznikami (?) ) – wszystko zależy od implementacji, ereg i preg (już pomijam fakt że składnia preg jest zgodna z RegExp’ami JS)

  6. Kamil Brenk 27 sierpnia 2010

    ereg pominąłem całkowicie, bo to przeżytek – jest wolniejsze, mniej bezpieczne i ogólnie nikomu niepotrzebne (manual: This function has been DEPRECATED as of PHP 5.3.0. Relying on this feature is highly discouraged. :D).

    Zrobiłem też test dla:

    1
    /(H|t)(T|t)(T|t)(P|p)/

    jednak stosując dodatkowo alternatywę (znaczek |) wyrażenia wykonywały się już znacząco wolniej (parę razy wolniej w stosunku do wyrażenia:

    1
    /[Hh][Tt][Tt][Pp]/

    dlatego od razu odpuściłem :-)

    Robiłem też kilka innych testów, ale myślę że te powyższe były najciekawsze i różnice były najbardziej widoczne :-)

    Wyrażenia regularne to ciężki kawałek chleba i niewątpliwie kiedyś jeszcze wrócę do nadgryzienia kolejnego kawałka :D

  7. Michal Wachowski 27 sierpnia 2010

    Jak na studiach będziesz mieć AS i DAS to wiele się wyjaśni – u mnie na polibudzie był od tego świetny prowadzący i to całe RegExpy stały się jasne :)

  8. Kamil Brenk 27 sierpnia 2010

    DAS / AS? Pierwsze słyszę :-) Raczej nie będę już miał takich wynalazków na studiach, niewiele tam się nauczyłem, co mogłoby mi się przydać w pracy programisty, niestety.

    Z tego co widziałem w planie to zostały mi chyba tylko bazy danych (Microsoft Access i podstawy SQL) :P

  9. Michal Wachowski 27 sierpnia 2010

    DAS – deterministyczne automaty skończone, AS – to samo ale bez D :)

  10. eN. 28 sierpnia 2010

    Problem w tym że nie zwracają tego samego, a dokładniej

    1 Grupowanie:
    a) z grupowaniem – mamy dostęp do podciągów
    b) bez grupowania – nie mamy dostępu do podciągów

    Cóż oczywistą sprawą jest że jeśli używasz czegoś co Ci nie jest potrzebne to będzie działało siłą rzeczy dłużej, więc po co to testować.

    2) Przechwytywanie

    Patrz pkt 1.

    Ciekawym porównaniem byłby wyrażenia w których musimy skorzystać z grupowania czyli przy używaniu operatora | a nie potrzebujemy przechwytywania: (czarny|czerwony)balonik vs (?:czarny|czerwony)balonik. Zwykle stosuje się to pierwsze bo ładniej wygląda, pytanie czy dużo przez to tracimy wydajności ;).

    3 Kwantyfikatory zachłanne
    a) zachłanne – „/.*:\/\/www\.testy-zawodowe\..*/”
    b) niezachłanne – „/http:\/\/www\.testy-zawodowe\.pl/”

    Kontrprzykład: „mam://.www.testy-zawodowe.cosnietak” pasuje do wzorca A nie pasuje do wzorca B, więc te wzorce są różne i ich porównanie mija się z celem.

    4) Kilka wystąpień tego samego ciągu

    a) zachłanne-1 – „/[0-9]+/”
    b) zachłanne-2 – „/[0-9]{9}/”
    c) niezachłanne – „/[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]/”

    Kontrprzykład: „123456787563″ pasuje do A, a nie pasuje do B , C (B i C są takie same). Więc porównanie B i C ma sens.

    A pojęcie (nie)zachłanności odnosi się do kwantyfikatorów i było nadużywane w powyższych dwóch testach, które raczej powinny się nazywać wyrażenie z kwantyfikatorem(3a,4a,4b) vs bez kwantyfikatora (3b,4c). Swoją drogą 4b zachłanne wcale nie jest.

    5) Jedyny test który jest wartościowy bo testuje dwie metody realizacji tego samego.

    I swoją drogą ciężko w wyrażeniach regularnych o dwa różne wzorce realizujące to samo na prostych przykładach, zwykle istnieje tylko jeden poprawny wzorzec.

  11. Kamil Brenk 29 sierpnia 2010

    @eN: Nie sposób się nie zgodzić z tym co piszesz, masz rację. Wezmę pod uwagę powyższe uwagi i przy kolejnych testach postaram się lepiej dobierać wyrażenia do testów – a na pewno jeszcze będę wracał do wyrażeń regularnych.

    Dziękuję więc za przydatne rady, lubię takie komentarze :-)



Dodaj komentarz

XHTML: Możesz użyć następujących tagów
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Kamil Brenk Blog

PHP, JavaScript, SQL, HTML

  • Informacje o blogu

    Kamil Brenk

    Blog o tworzeniu aplikacji na potrzeby sieci Web.

    Praktyczne przykłady, porady i sztuczki. PHP, SQL, AJAX, JavaScript, HTML i pochodne.

    Kanał RSS

    • Najnowsze
    • Komentarze
    • Popularne
    • Gramatyka w HTML i CSS
    • PHP kontra Microsoft Office, part I
    • Cross-Domain JavaScript: CORS
    • Wysyłanie wiadomości SMS w PHP
    • Boilerplate 2.0
    • Własne selektory w jQuery
    • Kamil Brenk: @Michał:1) jak już otrzymam dyplom to zrobię serię o...
    • Michal Wachowski: Po pierwsze - tyle czekania i tylko to? A bu! :) Po drugie -...
    • Kamil Brenk: @CapaciousCore: języki kompilowane są szybsze niż...
    • CapaciousCore: @Kamil Brenk wiem, że komentarze i post nie są uber świeże....
    • Kamil Brenk: @CapaciousCore: post i komentarze napisane ponad rok temu;...
    • CapaciousCore: Przebrnąłem przez te wszystkie komentarze i mam trochę...
    • Kamil Brenk: @arhiman: dzięki za komentarz :)A to dziwne co piszesz, bo...
    • Przyszłość PHP
    • Niestandardowe czcionki na stronie
    • Gramatyka w PHP, część 1
    • Umowa i zaliczka dla freelancera
    • Projekt aplikacji po stronie klienta
    • Własny mechanizm Feed
    • jQuery.extends dla PHP
  • Szukajka
    Wpisz co chcesz wyszukać na stronie…
  • Kategorie
    • Apache
    • Freelancer
    • Front-end Development
    • HTML5 & CSS3
    • Inne
    • JavaScript
    • Książki
    • PHP
    • Po godzinach
    • Pozycjonowanie
    • Protokół HTTP
    • SQL
    • Wyrażenia regularne
  • Moje serwisy
    • Testy zawodowe
    • Miłość, uczucia i seks
  • Czytane blogi
    • Wojciech Sznapka
    • Wojciech Soczyński
    • Michał Wachowski
    • Tomasz Kowalczyk
    • JavaScript po polsku | Code42
  • Archiwum
    • Luty 2012
    • Listopad 2011
    • Październik 2011
    • Wrzesień 2011
    • Sierpień 2011
    • Lipiec 2011
    • Maj 2011
    • Kwiecień 2011
    • Marzec 2011
    • Luty 2011
    • Styczeń 2011
    • Grudzień 2010
    • Listopad 2010
    • Październik 2010
    • Wrzesień 2010
    • Sierpień 2010
    • Lipiec 2010
    • Czerwiec 2010
    • Maj 2010
    • Kwiecień 2010
    • Marzec 2010
    • Luty 2010
    • Styczeń 2010
  • Strona główna
  • Curriculum Vitae
  • O mnie
  • Mapa strony
  • Kontakt

Kamil Brenk © 2010. All rights reserved.

Designed by FTL Wordpress Themes brought to you by Smashing Magazine.

Do góry ∧