¿Cómo funciona grep tan rápido?

113

Estoy realmente sorprendido por la funcionalidad de GREP en shell, antes solía usar el método de subcadena en java, pero ahora uso GREP para ello y se ejecuta en cuestión de segundos, es tremendamente más rápido que el código java que solía escribir. (aunque según mi experiencia podría estar equivocado)

Dicho esto, no he podido averiguar cómo está sucediendo. tampoco hay mucho disponible en la web.

Puede alguien ayudarme con esto?

Tipo
fuente
5
Es de código abierto, por lo que puedes echarle un vistazo. gnu.org/software/grep/devel.html
driis
6
Ridiculous Fish tiene un excelente artículo que responde exactamente a su pregunta: ridiculousfish.com/blog/posts/old-age-and-treachery.html
David Wolever
@WilliamPursell Cuando el tiempo de ejecución pasa en segundos, el JIT probablemente se haya calentado y la diferencia abrumadora se debe a que (1) grep es increíblemente inteligente sobre lo que hace y (2) el código Java es una elección de algoritmo bastante mala para el problema específico en el que se centra grep.
3
¿Cuánto tiempo dedica su implementación de Java a iniciar la JVM y cuánto tiempo realmente dedica a ejecutar su código? O podría ser una cuestión del algoritmo que usó en su código Java; Es probable que un algoritmo O (N ^ 2) sea lento en cualquier idioma.
Keith Thompson

Respuestas:

169

Asumiendo que su pregunta se refiere GNU grepespecíficamente. Aquí hay una nota del autor, Mike Haertel:

GNU grep es rápido porque EVITA MIRAR CADA BYTE DE ENTRADA.

GNU grep es rápido, ya que ejecuta instrucciones muy pocos para cada byte que lo hace ver.

GNU grep usa el conocido algoritmo de Boyer-Moore, que busca primero la letra final de la cadena de destino, y usa una tabla de búsqueda para decirle qué tan adelante puede saltar en la entrada cada vez que encuentra un carácter que no coincide.

GNU grep también desenrolla el bucle interno de Boyer-Moore y configura las entradas de la tabla delta de Boyer-Moore de tal manera que no es necesario realizar la prueba de salida del bucle en cada paso desenrollado. El resultado de esto es que, en el límite, GNU grep promedia menos de 3 instrucciones x86 ejecutadas por cada byte de entrada que realmente mira (y omite muchos bytes por completo).

GNU grep utiliza llamadas al sistema de entrada de Unix sin procesar y evita copiar datos después de leerlos. Además, GNU grep EVITA DIVULGAR LA ENTRADA EN LÍNEAS. Buscar nuevas líneas ralentizaría grep en un factor de varias veces, ¡porque para encontrar las nuevas líneas tendría que mirar cada byte!

Entonces, en lugar de usar entrada orientada a líneas, GNU grep lee datos sin procesar en un búfer grande, busca en el búfer usando Boyer-Moore, y solo cuando encuentra una coincidencia, busca los saltos de línea delimitadores (ciertas opciones de línea de comando como - n deshabilite esta optimización.)

Esta respuesta es un subconjunto de la información extraída de aquí .

Steve
fuente
41

Para agregar a la excelente respuesta de Steve.

Puede que no sea muy conocido, pero grep casi siempre es más rápido cuando se busca una cadena de patrón más larga que una corta, porque en un patrón más largo, Boyer-Moore puede saltar hacia adelante en pasos más largos para lograr velocidades sublineales aún mejores :

Ejemplo:

# 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

¡La forma más larga es un 35% más rápida!

¿Cómo? Boyer-Moore construye una tabla de salto hacia adelante a partir de la cadena de patrones, y siempre que hay una falta de coincidencia, elige el salto más largo posible (del último carácter al primero) antes de comparar un solo carácter en la entrada con el carácter en la tabla de salto.

Aquí hay un video que explica a Boyer Moore (Crédito a kommradHomer)

Otro error común (para GNU grep) es que fgrepes más rápido que grep. fin fgrepno significa 'rápido', significa 'fijo' (ver la página de manual), y dado que ambos son el mismo programa, y ​​ambos usan Boyer-Moore , no hay diferencia de velocidad entre ellos cuando se busca fijo- cadenas sin caracteres especiales regexp. La única que usar la razón fgrepes cuando hay un char especial expresión regular (como ., [], o *) No quiero que sea interpretado como tal. E incluso entonces grep -Fse prefiere la forma más portátil / estándar de fgrep.

arielf
fuente
3
Es intuitivo que los patrones más largos son más rápidos. Si el patrón era de un byte, grep tendría que verificar cada byte. Si el patrón es de 4 bytes, entonces podría hacer saltos de 4 bytes. Si el patrón fuera tan largo como el texto, grep solo haría un paso.
Noel
12
Sí, es intuitivo, si comprende cómo funciona Boyer-Moore.
arielf
2
Incluso de lo contrario, es intuitivo. Sería más fácil encontrar una aguja larga en un pajar que una más corta
RajatJ
2
El contraejemplo de "ser más rápido durante más tiempo" son los casos en los que tienes que hacer muchas pruebas antes de fallar y no puedes avanzar de todos modos. Digamos que el archivo xs.txtcontiene 100000000 'x, y lo hace grep yx xs.txt, entonces en realidad no puede encontrar una coincidencia antes que si lo hace grep yxxxxxxxxxxxxxxxxxxx xs.txt. La mejora de Boyer-Moore-Horspool a Boyer-Moore mejora el salto adelante en ese caso, pero probablemente no serán solo tres instrucciones de máquina en el caso general.
lrn
2
@Tino gracias. Sí, parece que los días en que (GNU) grep/fgrep/egreperan todos enlaces duros al mismo ejecutable se han ido. Ellos (y otras extensiones como las z*grep bz*greputilidades que se descomprimen sobre la marcha) son ahora pequeños envoltorios de shell grep. Algunos comentarios históricos interesantes sobre el cambio entre un solo ejecutable y envoltorios de shell se pueden encontrar en este compromiso: git.savannah.gnu.org/cgit/grep.git/commit/…
arielf