Jak grep działa tak szybko?

113

Jestem naprawdę zdumiony funkcjonalnością GREP w powłoce, wcześniej używałem metody podciąg w Javie, ale teraz używam do tego GREP i wykonuje się w ciągu kilku sekund, jest niesamowicie szybszy niż kod Java, który pisałem. (z mojego doświadczenia mogę się jednak mylić)

Biorąc to pod uwagę, nie byłem w stanie dowiedzieć się, jak to się dzieje? niewiele jest też dostępnych w sieci.

Czy ktoś może mi w tym pomóc?

Koleś
źródło
5
Jest to oprogramowanie typu open source, więc możesz sam się przekonać. gnu.org/software/grep/devel.html
driis
6
Ridiculous Fish ma świetny komentarz, odpowiadający dokładnie na Twoje pytanie: ridiculousfish.com/blog/posts/old-age-and-treachery.html
David Wolever
@WilliamPursell Kiedy czas wykonania spada w sekundach, JIT prawdopodobnie się rozgrzał, a otępiająca różnica wynika z tego, że (1) grep jest niesamowicie sprytny w tym, co robi i (2) kod Java dokonał dość złego wyboru algorytmu dla konkretnego problemu, na którym skupia się grep.
3
Ile czasu poświęca implementacji Java na uruchamianie maszyny JVM, a ile czasu faktycznie spędza na wykonywaniu kodu? Albo może to być kwestia algorytmu użytego w kodzie Java; algorytm O (N ^ 2) prawdopodobnie będzie działał wolno w każdym języku.
Keith Thompson,

Odpowiedzi:

169

Zakładając, że Twoje pytanie dotyczy GNU grepkonkretnie. Oto uwaga od autora, Mike'a Haertela:

GNU grep jest szybki, ponieważ UNIKANIE PATRZENIA NA KAŻDY WPROWADZONY BIT.

GNU grep jest szybki, ponieważ WYKONUJE BARDZO KILKA INSTRUKCJI DLA KAŻDEGO BITTU, na który patrzy.

GNU grep używa dobrze znanego algorytmu Boyera-Moore'a, który najpierw szuka ostatniej litery ciągu docelowego i używa tabeli przeglądowej, aby powiedzieć, jak daleko do przodu może pominąć dane wejściowe, gdy znajdzie niepasujący znak.

GNU grep rozwija również wewnętrzną pętlę Boyer-Moore i ustawia wpisy tablicy delta Boyera-Moore'a w taki sposób, że nie musi wykonywać testu wyjścia pętli na każdym rozwiniętym kroku. Wynikiem tego jest to, że w ramach limitu GNU grep uśrednia mniej niż 3 instrukcje x86 wykonywane dla każdego bajtu wejściowego, na który faktycznie patrzy (i całkowicie pomija wiele bajtów).

GNU grep używa surowych wywołań systemowych systemu Unix i unika kopiowania danych po ich odczytaniu. Co więcej, GNU grep UNIKANIE ŁAMANIA WEJŚCIA NA LINIE. Szukanie nowych linii spowolniłoby grep kilka razy, ponieważ aby znaleźć nowe linie, musiałby spojrzeć na każdy bajt!

Więc zamiast korzystać z wejścia zorientowanego liniowo, GNU grep czyta surowe dane do dużego bufora, przeszukuje bufor używając Boyer-Moore i tylko wtedy, gdy znajdzie dopasowanie, idzie i szuka ograniczających znaki nowej linii (niektóre opcje wiersza poleceń, takie jak - n wyłącz tę optymalizację.)

Ta odpowiedź jest podzbiorem informacji zaczerpniętych stąd .

Steve
źródło
41

Aby dodać do doskonałej odpowiedzi Steve'a.

Być może nie jest to powszechnie znane, ale grep jest prawie zawsze szybszy podczas grepowania dłuższego wzoru niż krótkiego, ponieważ w dłuższym wzorze Boyer-Moore może przeskoczyć do przodu w dłuższych krokach, aby osiągnąć jeszcze lepsze prędkości podliniowe :

Przykład:

# after running these twice to ensure apples-to-apples comparison
# (everything is in the buffer cache) 

$ time grep -c 'tg=f_c' 20140910.log
28
0.168u 0.068s 0:00.26

$ time grep -c ' /cc/merchant.json tg=f_c' 20140910.log
28
0.100u 0.056s 0:00.17

Dłuższa forma jest o 35% szybsza!

Dlaczego? Boyer-Moore konstruuje tablicę pomijania do przodu z łańcucha wzorca, a gdy występuje niedopasowanie, wybiera najdłuższe możliwe pominięcie (od ostatniego znaku do pierwszego) przed porównaniem pojedynczego znaku na wejściu do znaku w tabeli pominięcia.

Oto wideo wyjaśniające Boyer Moore (Credit to kommradHomer)

Innym powszechnym nieporozumieniem (w przypadku GNU grep) jest to, że fgrepjest szybszy niż grep. fin fgrepnie oznacza `` fast '', oznacza `` fixed '' (patrz strona podręcznika), a ponieważ oba są tym samym programem i oba używają Boyer-Moore , nie ma różnicy w szybkości między nimi podczas wyszukiwania ustalonych- ciągi bez znaków specjalnych wyrażenia regularnego. Jedynym powodem, dla którego użycie fgrepjest wtedy, gdy istnieje regexp specjalnym char (jak ., []lub *) Nie ma to być interpretowane jako takie. I nawet wtedy bardziej przenośna / standardowa forma grep -Fjest preferowana fgrep.

arielf
źródło
3
To intuicyjne, że dłuższe wzory są szybsze. Gdyby wzorzec był jednobajtowy, grep musiałby sprawdzić każdy bajt. Jeśli wzorzec ma 4 bajty, może wykonać 4-bajtowe pominięcia. Gdyby wzorzec był tak długi jak tekst, to grep wykonałby tylko jeden krok.
noel
12
Tak, jest intuicyjny - jeśli rozumiesz, jak działa Boyer-Moore.
arielf
2
Nawet poza tym jest intuicyjny. Łatwiej byłoby znaleźć długą igłę w stogu siana niż krótszą
RajatJ
2
Przykładem przeciwstawnym do „bycia dłuższym, gdy jest szybszy” są przypadki, w których musisz wykonać wiele testów, zanim się nie uda, a i tak nie możesz iść do przodu. Powiedzmy, że plik xs.txtzawiera 100000000 'x, a zrobisz to grep yx xs.txt, wtedy faktycznie nie znajdzie dopasowania wcześniej niż gdybyś to zrobił grep yxxxxxxxxxxxxxxxxxxx xs.txt. Ulepszenie Boyer-Moore-Horspool do Boyer-Moore poprawia w tym przypadku skok do przodu, ale prawdopodobnie nie będą to tylko trzy instrukcje maszynowe w ogólnym przypadku.
lrn
2
@Tino dzięki. Tak, wygląda na to, że czasy, w których (GNU) grep/fgrep/egrepbyło wszystkimi twardymi dowiązaniami do tego samego pliku wykonywalnego, minęły. One (i inne rozszerzenia, takie jak narzędzia, z*grep bz*grepktóre dekompresują się w locie), są teraz małymi opakowaniami grep. Kilka interesujących komentarzy historycznych na temat przełączania się między pojedynczym plikiem wykonywalnym a opakowaniami powłoki można znaleźć w tym zatwierdzeniu: git.savannah.gnu.org/cgit/grep.git/commit/…
arielf