¿Cuál es el algoritmo de búsqueda de subcadenas más rápido?


165

OK, así que no sueno como un idiota, voy a exponer el problema / requisitos más explícitamente:

  • La aguja (patrón) y el pajar (texto para buscar) son cadenas terminadas en nulo de estilo C. No se proporciona información de longitud; si es necesario, debe calcularse.
  • La función debe devolver un puntero a la primera coincidencia, o NULLsi no se encuentra ninguna coincidencia.
  • Los casos de falla no están permitidos. Esto significa que cualquier algoritmo con requisitos de almacenamiento no constantes (o grandes constantes) necesitará tener un caso de respaldo para la falla de asignación (y el rendimiento en la atención de respaldo por lo tanto contribuye al rendimiento en el peor de los casos).
  • La implementación debe realizarse en C, aunque una buena descripción del algoritmo (o enlace a dicho) sin código también está bien.

... así como lo que quiero decir con "más rápido":

  • Determinista O(n)donde n= longitud del pajar. (Pero puede ser posible usar ideas de algoritmos que normalmente sonO(nm) (por ejemplo, hash rodante) si se combinan con un algoritmo más robusto para dar O(n)resultados deterministas ).
  • Nunca funciona (medible; un par de relojes para if (!needle[1]) etc. están bien) peor que el ingenuo algoritmo de fuerza bruta, especialmente en agujas muy cortas, que probablemente sean el caso más común. (La sobrecarga de preprocesamiento pesado incondicional es mala, al igual que tratar de mejorar el coeficiente lineal para agujas patológicas a expensas de las agujas probables).
  • Dada una aguja arbitraria y un pajar, un rendimiento comparable o mejor (no peor que un 50% más de tiempo de búsqueda) en comparación con cualquier otro algoritmo ampliamente implementado.
  • Aparte de estas condiciones, estoy dejando la definición de "más rápido" abierta. Una buena respuesta debería explicar por qué considera que el enfoque que sugiere es "más rápido".

Mi implementación actual se ejecuta aproximadamente entre un 10% más lento y 8 veces más rápido (dependiendo de la entrada) que la implementación de Two-Way de glibc.

Actualización: mi algoritmo óptimo actual es el siguiente:

  • Para agujas de longitud 1, use strchr.
  • Para agujas de longitud 2-4, use palabras de máquina para comparar 2-4 bytes a la vez de la siguiente manera: precargue la aguja en un entero de 16 o 32 bits con desplazamientos de bits y ciclo de bytes viejos / bytes nuevos desde el pajar en cada iteración . Cada byte del pajar se lee exactamente una vez e incurre en una comprobación contra 0 (final de la cadena) y una comparación de 16 o 32 bits.
  • Para agujas de longitud> 4, use el algoritmo de dos vías con una tabla de desplazamiento incorrecta (como Boyer-Moore) que se aplica solo al último byte de la ventana. Para evitar la sobrecarga de inicializar una tabla de 1kb, lo que sería una pérdida neta para muchas agujas de longitud moderada, mantengo una matriz de bits (32 bytes) que marca qué entradas en la tabla de desplazamiento se inicializan. Los bits que no están establecidos corresponden a valores de bytes que nunca aparecen en la aguja, para los cuales es posible un desplazamiento de longitud de aguja completa.

Las grandes preguntas que me quedan en la mente son:

  • ¿Hay alguna manera de hacer un mejor uso de la tabla de turnos malos? Boyer-Moore lo aprovecha al escanear hacia atrás (de derecha a izquierda), pero Two-Way requiere un escaneo de izquierda a derecha.
  • Los únicos dos algoritmos candidatos viables que he encontrado para el caso general (sin condiciones de falta de memoria o de rendimiento cuadrático) son la coincidencia bidireccional y de cadenas en alfabetos ordenados . ¿Pero hay casos fácilmente detectables en los que diferentes algoritmos serían óptimos? Ciertamente, muchos de los algoritmos O(m)(donde mestá la longitud de la aguja) en el espacio podrían usarse para más m<100o menos. También sería posible usar algoritmos que son el peor de los casos, si hay una prueba fácil para las agujas que probablemente solo requieren tiempo lineal.

Puntos de bonificación por:

  • ¿Puedes mejorar el rendimiento asumiendo que la aguja y el pajar son UTF-8 bien formados? (Con caracteres de diferentes longitudes de byte, la buena forma- imposición impone algunos requisitos de alineación de la cuerda entre la aguja y el pajar y permite cambios automáticos de 2-4 bytes cuando se encuentra un byte de cabecera que no coincide. Pero estas restricciones le compran mucho / algo más allá de lo que cálculos de sufijos máximos, buenos cambios de sufijos, etc. ¿ya te dan varios algoritmos?)

Nota: conozco la mayoría de los algoritmos que existen, pero no sé qué tan bien funcionan en la práctica. Aquí hay una buena referencia para que las personas no me sigan dando referencias sobre algoritmos como comentarios / respuestas: http://www-igm.univ-mlv.fr/~lecroq/string/index.html


Hay bastantes algoritmos de búsqueda de cadenas listados en Algoritmos en cadenas . Es posible que desee describir qué algoritmos ha considerado de esta lista.
Greg Hewgill el

61
¡Ese enlace al final es oro!
Carlos

44
No puedo creer que aún no hayas aceptado una respuesta.
user541686

1
@Mehrdad: Estaba a punto de decir que no hay respuestas que realmente aborden la pregunta tal como se hizo, pero la suya parece. En el momento en que respondiste, había seguido adelante y dejé más mejoras strstrcomo algo para más adelante, por lo que no he podido leer correctamente el documento que vinculaste, pero suena muy prometedor. Gracias y perdón por no volver a usted.
R .. GitHub DEJA DE AYUDAR A ICE

Respuestas:


37

Construya una biblioteca de prueba de posibles agujas y pajares. Perfile las pruebas en varios algoritmos de búsqueda, incluida la fuerza bruta. Elija el que mejor funcione con sus datos.

Boyer-Moore usa una tabla de caracteres malos con una buena tabla de sufijos.

Boyer-Moore-Horspool usa una tabla de personajes malos.

Knuth-Morris-Pratt usa una tabla de coincidencias parciales.

Rabin-Karp usa hashes en ejecución.

Todos intercambian gastos generales por comparaciones reducidas en un grado diferente, por lo que el rendimiento del mundo real dependerá de las longitudes promedio de la aguja y el pajar. Cuanto más sobrecarga inicial, mejor con entradas más largas. Con agujas muy cortas, la fuerza bruta puede ganar.

Editar:

Un algoritmo diferente podría ser mejor para encontrar pares de bases, frases en inglés o palabras sueltas. Si hubiera un mejor algoritmo para todas las entradas, se habría publicado.

Piensa en la siguiente mesita. Cada signo de interrogación puede tener un mejor algoritmo de búsqueda diferente.

                 short needle     long needle
short haystack         ?               ?
long haystack          ?               ?

Esto realmente debería ser un gráfico, con un rango de entradas más cortas a más largas en cada eje. Si trazó cada algoritmo en dicho gráfico, cada uno tendría una firma diferente. Algunos algoritmos sufren con mucha repetición en el patrón, lo que puede afectar usos como la búsqueda de genes. Algunos otros factores que afectan el rendimiento general son la búsqueda del mismo patrón más de una vez y la búsqueda de diferentes patrones al mismo tiempo.

Si necesito un conjunto de muestra, creo que eliminaría un sitio como google o wikipedia, luego eliminaría el html de todas las páginas de resultados. Para un sitio de búsqueda, escriba una palabra y luego use una de las frases de búsqueda sugeridas. Elija algunos idiomas diferentes, si corresponde. Usando páginas web, todos los textos serían de cortos a medianos, así que combine páginas suficientes para obtener textos más largos. También puede encontrar libros de dominio público, registros legales y otros grandes cuerpos de texto. O simplemente genera contenido aleatorio eligiendo palabras de un diccionario. Pero el punto de creación de perfiles es probar el tipo de contenido que buscará, por lo tanto, use muestras del mundo real si es posible.

Me fui corto y largo vago. Para la aguja, pienso en corto como menos de 8 caracteres, medio como menos de 64 caracteres y largo como menos de 1k. Para el pajar, pienso en corto como debajo de 2 ^ 10, medio como debajo de 2 ^ 20 y largo como hasta 2 ^ 30 caracteres.


1
¿Tiene buenas sugerencias para una biblioteca de prueba? La pregunta anterior que hice sobre SO estaba relacionada con eso y nunca obtuve respuestas reales. (excepto el mío ...) Debería ser extenso. Incluso si mi idea de una aplicación para strstr es buscar texto en inglés, alguien más podría estar buscando genes en secuencias de pares de bases ...
R .. GitHub STOP HELPING ICE

3
Es un poco más complicado que corto / largo. Para la aguja, las grandes preguntas relevantes para el desempeño de la mayoría de los algoritmos son: ¿Longitud? ¿Hay alguna periodicidad? ¿La aguja contiene todos los caracteres únicos (sin repeticiones)? ¿O todo el mismo personaje? ¿Hay una gran cantidad de personajes en el pajar que nunca aparecen en la aguja? ¿Existe la posibilidad de tener que lidiar con agujas provistas por un atacante que quiere explotar el peor de los casos para dañar su sistema? Etc ..
R .. GitHub DEJA DE AYUDAR AL HIELO

31

Publicado en 2011, creo que podría ser el algoritmo "Coincidencia simple de cadenas de espacio constante en tiempo real" de Dany Breslauer, Roberto Grossi y Filippo Mignosi.

Actualizar:

En 2014, los autores publicaron esta mejora: Hacia una coincidencia de cadena empaquetada óptima .


1
Wow gracias. Estoy leyendo el periodico. Si resulta ser mejor que lo que tengo, definitivamente aceptaré tu respuesta.
R .. GitHub DEJA DE AYUDAR AL HIELO

1
@R ..: ¡Claro! :) Hablando de eso, si logras implementar el algoritmo, considera publicarlo en StackOverflow para que todos puedan beneficiarse de él. No he encontrado ninguna implementación de él en ningún lado y no soy bueno implementando algoritmos que encuentro en trabajos de investigación jaja.
user541686

2
Es una variante del algoritmo "bidireccional" que ya estoy usando, por lo que adaptar mi código para usar esto en realidad podría ser fácil. Sin embargo, tendré que leer el documento con más detalle para estar seguro, y necesito evaluar si los cambios realizados son compatibles con mi uso de una "tabla de caracteres malos" que acelera enormemente el caso común.
R .. GitHub DEJA DE AYUDAR AL HIELO

11
¡Y todavía no has aceptado la respuesta de @ Mehrdad! :-)
lifebalance

3
@DavidWallace: ¿Qué? Tiene los títulos en papel y los autores. Incluso si el enlace se corta, puedes encontrar los documentos. ¿Qué esperas que haga? ¿Escribir pseudocódigo para el algoritmo? ¿Qué te hace pensar que entiendo el algoritmo?
user541686

23

El http://www-igm.univ-mlv.fr/~lecroq/string/index.html enlace que apunta es una excelente fuente y resumen de algunos de los algoritmos de coincidencia de cadenas más conocidos e investigados.

Las soluciones a la mayoría de los problemas de búsqueda incluyen compensaciones con respecto a los requisitos generales de procesamiento previo, tiempo y espacio. Ningún algoritmo único será óptimo o práctico en todos los casos.

Si su objetivo es diseñar un algoritmo específico para la búsqueda de cadenas, ignore el resto de lo que tengo que decir: Si desea desarrollar una rutina de servicio de búsqueda de cadenas generalizada, intente lo siguiente:

Dedique un tiempo a revisar las fortalezas y debilidades específicas de los algoritmos que ya ha mencionado. Realice la revisión con el objetivo de encontrar un conjunto de algoritmos que cubran el rango y el alcance de las búsquedas de cadenas que le interesen. Luego, cree un selector de búsqueda frontal basado en una función de clasificador para apuntar al mejor algoritmo para las entradas dadas. De esta manera, puede emplear el algoritmo más eficiente para hacer el trabajo. Esto es particularmente efectivo cuando un algoritmo es muy bueno para ciertas búsquedas pero se degrada mal. Por ejemplo, la fuerza bruta es probablemente la mejor para agujas de longitud 1, pero se degrada rápidamente a medida que aumenta la longitud de la aguja, con lo cual el algoritmo sustik-moorepuede volverse más eficiente (sobre alfabetos pequeños), luego, para agujas más largas y alfabetos más grandes, los algoritmos KMP o Boyer-Moore pueden ser mejores. Estos son solo ejemplos para ilustrar una posible estrategia.

El enfoque de algoritmo múltiple no es una idea nueva. Creo que ha sido utilizado por algunos paquetes comerciales de clasificación / búsqueda (por ejemplo, SYNCSORT, comúnmente utilizado en mainframes, implementa varios algoritmos de clasificación y utiliza la heurística para elegir el "mejor" para las entradas dadas)

Cada algoritmo de búsqueda viene en varias variaciones que pueden marcar diferencias significativas en su rendimiento, como, por ejemplo, ilustra este artículo .

Compare su servicio para clasificar las áreas donde se necesitan estrategias de búsqueda adicionales o para ajustar de manera más efectiva su función de selección. Este enfoque no es rápido ni fácil, pero si se hace bien puede producir muy buenos resultados.


1
Gracias por la respuesta, especialmente el enlace a Sustik-Moore que no había visto antes. El enfoque de algoritmos múltiples seguramente es de uso generalizado. Glibc básicamente hace strchr, bidireccional sin tabla de desplazamiento de caracteres incorrectos o bidireccional con tabla de desplazamiento de caracteres defectuosos, dependiendo de si needle_len es 1, <32 o> 32. Mi enfoque actual es el mismo, excepto que siempre uso la tabla de turnos; Reemplacé el memset de 1kb necesario para hacerlo con un memset de 32 bytes en un bitset usado para marcar qué elementos de la tabla se han inicializado, y obtengo el beneficio (pero no la sobrecarga) incluso para agujas pequeñas.
R .. GitHub DEJA DE AYUDAR AL HIELO

1
Después de pensarlo, tengo curiosidad por saber cuál es la aplicación prevista para Sustik-Moore. Con alfabetos pequeños, nunca podrá hacer cambios significativos (todos los caracteres del alfabeto seguramente aparecerán cerca del final de la aguja) y los enfoques de autómatas finitos son muy eficientes (tabla de transición de estado pequeño). Así que no puedo imaginar ningún escenario en el que Sustik-Moore pueda ser óptimo ...
R .. GitHub

Gran respuesta: si pudiera destacar esta respuesta en particular, lo haría.
Jason S

1
@R .. La teoría detrás del algoritmo sustik-moore es que debería darte mayores cantidades de desplazamiento promedio cuando la aguja es relativamente grande y el alfabeto es relativamente pequeño (por ejemplo, buscando secuencias de ADN). Más grande en este caso solo significa más grande que el algoritmo básico de Boyer-Moore produciría dadas las mismas entradas. Es mucho más difícil decir cuánto más eficiente es esto en relación con un enfoque de autómatas finitos o con alguna otra variación de Boyer-Moore (de la cual hay muchas). Es por eso que enfaticé dedicar algo de tiempo a investigar las fortalezas / debilidades específicas de sus algoritmos candidatos.
NealB

1
Hm, supongo que me quedé atrapado pensando en cambios solo en el sentido de cambios de mal carácter de Boyer-Moore. Sin embargo, con una mejora en los cambios de sufijo BM buenos, Sustik-Moore posiblemente podría superar los enfoques DFA para la búsqueda de ADN. Cosas ordenadas.
R .. GitHub DEJA DE AYUDAR A HIELO

21

Me sorprendió ver nuestro informe técnico citado en esta discusión; Soy uno de los autores del algoritmo que se llamó anteriormente Sustik-Moore. (No utilizamos ese término en nuestro artículo).

Quería enfatizar que para mí la característica más interesante del algoritmo es que es bastante simple demostrar que cada letra se examina como máximo una vez. Para versiones anteriores de Boyer-Moore, probaron que cada letra se examina como máximo 3 y más tarde 2 veces como máximo, y esas pruebas estaban más involucradas (ver citas en papel). Por lo tanto, también veo un valor didáctico al presentar / estudiar esta variante.

En el documento también describimos variaciones adicionales que están orientadas hacia la eficiencia mientras se relajan las garantías teóricas. Es un documento corto y el material debe ser comprensible para un graduado promedio de secundaria en mi opinión.

Nuestro objetivo principal era llevar esta versión a la atención de otros que puedan mejorarla aún más. La búsqueda de cadenas tiene tantas variaciones y nosotros solos no podemos pensar en todos dónde esta idea podría traer beneficios. (Texto fijo y patrón cambiante, texto diferente de patrón fijo, preprocesamiento posible / no posible, ejecución paralela, búsqueda de subconjuntos coincidentes en textos grandes, permitir errores, coincidencias cercanas, etc., etc.)


1
¿Conoces una implementación de C o C ++ disponible? Estoy pensando en usar esto para alguna búsqueda de motivos de ADN (coincidencias de motivos exactos). Si no, tal vez intente desarrollar una implementación yo mismo y enviarla para impulsar el algoritmo
JDiMatteo

44
Sin una implementación disponible conocida, parece improbable que el algoritmo Sustik-Moore / 2BLOCK se use en la práctica y continúe omitiéndose de los resultados en documentos de resumen como "El problema de coincidencia de cadenas exactas: una evaluación experimental exhaustiva"
JDiMatteo

18

El algoritmo de búsqueda de subcadenas más rápido dependerá del contexto:

  1. el tamaño del alfabeto (p. ej., ADN vs inglés)
  2. la longitud de la aguja

El documento de 2010 "El problema de coincidencia de cadenas exactas: una evaluación experimental integral" ofrece tablas con tiempos de ejecución para 51 algoritmos (con diferentes tamaños de alfabeto y longitudes de aguja), para que pueda elegir el mejor algoritmo para su contexto.

Todos esos algoritmos tienen implementaciones en C, así como un conjunto de pruebas, aquí:

http://www.dmi.unict.it/~faro/smart/algorithms.php


4

Una muy buena pregunta. Solo agrega algunos pedacitos ...

  1. Alguien hablaba de la secuencia de ADN. Pero para la secuencia de ADN, lo que solemos hacer es construir una estructura de datos (por ejemplo, matriz de sufijos, árbol de sufijos o índice FM) para el pajar y unir muchas agujas contra ella. Esta es una pregunta diferente.

  2. Sería genial si alguien quisiera comparar varios algoritmos. Hay muy buenos puntos de referencia sobre la compresión y la construcción de matrices de sufijos, pero no he visto un punto de referencia sobre la coincidencia de cadenas. Los posibles candidatos de pajar podrían ser del punto de referencia SACA .

  3. Hace unos días estaba probando la implementación de Boyer-Moore desde la página que me recomendó (EDITAR: necesito una llamada de función como memmem (), pero no es una función estándar, así que decidí implementarla). Mi programa de evaluación comparativa utiliza un pajar aleatorio. Parece que la implementación de Boyer-Moore en esa página es más rápida que la memmem () de glibc y strnstr () de Mac. En caso de que esté interesado, la implementación está aquí y el código de evaluación comparativa está aquí . Definitivamente, este no es un punto de referencia realista, pero es un comienzo.


Si tiene algunas buenas agujas para probar junto con los candidatos de pajar del punto de referencia SACA, publíquelos como respuesta a mi otra pregunta y, a menos que obtenga una mejor respuesta, lo marcaré como aceptado.
R .. GitHub DEJA DE AYUDAR AL HIELO

3
Sobre su memmem y Boyer-Moore, es muy probable que Boyer-Moore (o más bien una de las mejoras de Boyer-Moore) tenga un mejor rendimiento en datos aleatorios. Los datos aleatorios tienen una probabilidad extremadamente baja de periodicidad y coincidencias parciales largas que conducen al peor de los casos cuadrático. Estoy buscando una forma de combinar Boyer-Moore y Two-Way o para detectar de manera eficiente cuándo Boyer-Moore es "seguro de usar", pero hasta ahora no he tenido éxito. Por cierto, no usaría memmem de glibc como comparación. Mi implementación de lo que básicamente es el mismo algoritmo que glibc es varias veces más rápida.
R .. GitHub DEJA DE AYUDAR AL HIELO

Como dije, no es mi implementación. Crédito a Christian Charras y Thierry Lecroq. Puedo imaginar por qué la entrada aleatoria es mala para la evaluación comparativa y estoy seguro de que glibc elige algoritmos por razones. También supongo que memmem () no se implementa de manera eficiente. Intentaré. Gracias.
user172818

4

Sé que es una pregunta antigua, pero la mayoría de las tablas de cambio malas son de un solo carácter. Si tiene sentido para su conjunto de datos (por ejemplo, especialmente si se trata de palabras escritas), y si tiene el espacio disponible, puede obtener una aceleración dramática utilizando una tabla de cambio incorrecta hecha de n-gramos en lugar de caracteres individuales.


3

Use stdlib strstr:

char *foundit = strstr(haystack, needle);

Fue muy rápido, solo tardé unos 5 segundos en escribir.


26
Y si lees mi pregunta, verás que me fue muy fácil superarla. Sin embargo, me gusta tu sarcasmo, me saltearé el -1.
R .. GitHub DEJA DE AYUDAR AL HIELO

3

Aquí está la implementación de búsqueda de Python , utilizada desde todo el núcleo. Los comentarios indican que utiliza una tabla comprimida boyer-moore delta 1 .

Yo mismo he realizado una experimentación bastante extensa con la búsqueda de cadenas, pero fue para múltiples cadenas de búsqueda. Las implementaciones de ensamblaje de Horspool y Bitap a menudo pueden defenderse contra algoritmos como Aho-Corasick para bajos recuentos de patrones.


3

Un strchralgoritmo más rápido de "Búsqueda de un solo carácter coincidente" (ala ).

Notas importantes:

  • Estas funciones usan un gcccompilador "número / recuento de ceros (iniciales | finales)" intrínseco __builtin_ctz. Es probable que estas funciones solo sean rápidas en máquinas que tienen una o más instrucciones que realizan esta operación (es decir, x86, ppc, arm).

  • Estas funciones suponen que la arquitectura de destino puede realizar cargas no alineadas de 32 y 64 bits. Si su arquitectura de destino no es compatible con esto, deberá agregar algo de lógica de inicio para alinear correctamente las lecturas.

  • Estas funciones son neutrales al procesador. Si la CPU de destino tiene instrucciones vectoriales, es posible que pueda hacerlo (mucho) mejor. Por ejemplo, la strlensiguiente función usa SSE3 y puede modificarse trivialmente a XOR los bytes escaneados para buscar un byte que no sea 0. Los puntos de referencia realizados en una computadora portátil Core 2 de 2.66 GHz con Mac OS X 10.6 (x86_64):

    • 843.433 MB / s para strchr
    • 2656.742 MB / s para findFirstByte64
    • 13094.479 MB / s para strlen

... una versión de 32 bits:

#ifdef __BIG_ENDIAN__
#define findFirstZeroByte32(x) ({ uint32_t _x = (x); _x = ~(((_x & 0x7F7F7F7Fu) + 0x7F7F7F7Fu) | _x | 0x7F7F7F7Fu); (_x == 0u)   ? 0 : (__builtin_clz(_x) >> 3) + 1; })
#else
#define findFirstZeroByte32(x) ({ uint32_t _x = (x); _x = ~(((_x & 0x7F7F7F7Fu) + 0x7F7F7F7Fu) | _x | 0x7F7F7F7Fu);                    (__builtin_ctz(_x) + 1) >> 3; })
#endif

unsigned char *findFirstByte32(unsigned char *ptr, unsigned char byte) {
  uint32_t *ptr32 = (uint32_t *)ptr, firstByte32 = 0u, byteMask32 = (byte) | (byte << 8);
  byteMask32 |= byteMask32 << 16;
  while((firstByte32 = findFirstZeroByte32((*ptr32) ^ byteMask32)) == 0) { ptr32++; }
  return(ptr + ((((unsigned char *)ptr32) - ptr) + firstByte32 - 1));
}

... y una versión de 64 bits:

#ifdef __BIG_ENDIAN__
#define findFirstZeroByte64(x) ({ uint64_t _x = (x); _x = ~(((_x & 0x7F7F7F7F7f7f7f7full) + 0x7F7F7F7F7f7f7f7full) | _x | 0x7F7F7F7F7f7f7f7full); (_x == 0ull) ? 0 : (__builtin_clzll(_x) >> 3) + 1; })
#else
#define findFirstZeroByte64(x) ({ uint64_t _x = (x); _x = ~(((_x & 0x7F7F7F7F7f7f7f7full) + 0x7F7F7F7F7f7f7f7full) | _x | 0x7F7F7F7F7f7f7f7full);                    (__builtin_ctzll(_x) + 1) >> 3; })
#endif

unsigned char *findFirstByte64(unsigned char *ptr, unsigned char byte) {
  uint64_t *ptr64 = (uint64_t *)ptr, firstByte64 = 0u, byteMask64 = (byte) | (byte << 8);
  byteMask64 |= byteMask64 << 16;
  byteMask64 |= byteMask64 << 32;
  while((firstByte64 = findFirstZeroByte64((*ptr64) ^ byteMask64)) == 0) { ptr64++; }
  return(ptr + ((((unsigned char *)ptr64) - ptr) + firstByte64 - 1));
}

Editar 2011/06/04 El OP señala en los comentarios que esta solución tiene un "error insuperable":

puede leer más allá del byte buscado o del terminador nulo, que podría acceder a una página o página sin asignar sin permiso de lectura. Simplemente no puede usar lecturas grandes en funciones de cadena a menos que estén alineadas.

Esto es técnicamente cierto, pero se aplica a prácticamente cualquier algoritmo que opera en fragmentos que son más grandes que un solo byte, incluido el método sugerido por el OP en los comentarios:

Una strchrimplementación típica no es ingenua, pero es un poco más eficiente que lo que diste. Vea el final de esto para el algoritmo más utilizado: http://graphics.stanford.edu/~seander/bithacks.html#ZeroInWord

Tampoco tiene nada que ver con la alineación. per se. Es cierto que esto podría causar el comportamiento discutido en la mayoría de las arquitecturas comunes en uso, pero esto tiene más que ver con los detalles de implementación de microarquitectura: si la lectura no alineada se extiende a un límite de 4K (nuevamente, típico), entonces esa lectura causará un programa falla de terminación si el siguiente límite de página 4K no está mapeado.

Pero esto no es un "error" en el algoritmo dado en la respuesta; ese comportamiento se debe a que funciona como strchry strlenno acepta un lengthargumento para limitar el tamaño de la búsqueda. La búsqueda char bytes[1] = {0x55};, que para los fines de nuestra discusión se coloca al final de un límite de página de VM de 4K y la siguiente página no está asignada, con strchr(bytes, 0xAA)(donde strchrhay una implementación de byte a la vez) se bloqueará exactamente el mismo camino. Lo mismo para strchrprimo relacionadostrlen .

Sin un lengthargumento, no hay forma de saber cuándo debe cambiar el algoritmo de alta velocidad y volver a un algoritmo byte por byte. Un "error" mucho más probable sería leer "más allá del tamaño de la asignación", lo que técnicamente resulta de undefined behavioracuerdo con los diversos estándares del lenguaje C, y sería marcado como un error por algo comovalgrind .

En resumen, todo lo que opera en fragmentos de bytes más grandes para ir más rápido, como lo hace este código de respuestas y el código señalado por el OP, pero que debe tener una semántica de lectura con precisión de byte, es probable que sea "defectuoso" si no hay length argumento para controle los casos de esquina de "la última lectura".

El código en esta respuesta es un núcleo para poder encontrar el primer byte en un fragmento de tamaño de palabra de CPU natural rápidamente si la CPU de destino tiene una ctzinstrucción rápida similar. Es trivial agregar cosas como asegurarse de que solo funcione en límites naturales correctamente alineados, o alguna forma delength límite, lo que le permitiría salir del núcleo de alta velocidad y pasar a una verificación byte por byte más lenta.

El OP también dice en los comentarios:

En cuanto a su optimización ctz, solo hace una diferencia para la operación de cola O (1). Podría mejorar el rendimiento con cadenas pequeñas (por ejemplo, strchr("abc", 'a');pero ciertamente no con cadenas de cualquier tamaño mayor.

Que esta afirmación sea cierta o no depende en gran medida de la microarquitectura en cuestión. Usando el modelo canónico de tubería RISC de 4 etapas, entonces es casi seguro que es cierto. Pero es extremadamente difícil saber si es cierto para una CPU súper escalar contemporánea fuera de servicio, donde la velocidad del núcleo puede eclipsar por completo la velocidad de transmisión de memoria. En este caso, no solo es plausible, sino bastante común, que haya una gran brecha en "el número de instrucciones que se pueden retirar" en relación con "el número de bytes que se pueden transmitir" para que tenga "el número de instrucciones que se pueden retirar para cada byte que se puede transmitir ". Si es lo suficientemente grande, la ctzinstrucción + shift se puede hacer "gratis".


"Para agujas de longitud 1, use strchr." - Usted solicitó los algoritmos de búsqueda de subcadenas más rápidos. Encontrar una subcadena de longitud 1 es solo un caso especial, uno que también se puede optimizar. Si cambia su código de caso especial actual por subcadenas de longitud 1 ( strchr) con algo como lo anterior, las cosas (posiblemente, dependiendo de cómo strchrse implementen) irán más rápido. El algoritmo anterior es casi 3 veces más rápido que una strchrimplementación ingenua típica .
johne

2
OP dijo que la cadena estaba correctamente terminada en nulo, por lo que su discusión char bytes[1] = {0x55};es irrelevante. Muy relevante es su comentario acerca de que esto sea cierto para cualquier algoritmo de lectura de palabras que no conozca la longitud de antemano.
Seth Robertson

1
El problema no se aplica a la versión que cité porque solo lo usa en punteros alineados, al menos eso es lo que hacen las implementaciones correctas.
R .. GitHub DEJA DE AYUDAR A ICE

2
@R, no tiene nada que ver con "punteros alineados". Hipotéticamente, si tenía una arquitectura que admitía la protección de VM con granularidad de nivel de byte, y cada mallocasignación estaba "suficientemente acolchada" a cada lado y el sistema VM aplicaba protección granular de byte para esa asignación ... si el puntero está alineado suponiendo que intla alineación natural trivial de 32 bits ) sea discutible, todavía es posible que esa lectura alineada lea más allá del tamaño de la asignación. CUALQUIER lectura más allá del tamaño de la asignación es undefined behavior.
johne

55
@johne: +1 para comentar. Conceptualmente tiene razón, pero la realidad es que las protecciones de granularidad de bytes son tan caras tanto para almacenar como para hacer cumplir que no existen y nunca existirán. Si sabe que el almacenamiento subyacente son asignaciones de granularidad de página obtenidas del equivalente de mmap, entonces la alineación es suficiente.
R .. GitHub DEJA DE AYUDAR A ICE

3

Simplemente busque "strstr más rápido", y si ve algo de interés, pregúnteme.

En mi opinión, se imponen demasiadas restricciones (sí, todos queremos lineal sub-lineal en el buscador máximo), sin embargo, se necesita un programador real para intervenir, hasta entonces creo que el enfoque hash es simplemente una solución ingeniosa (limbo) bien reforzado por BNDM para 2..16 patrones más cortos).

Solo un ejemplo rápido:

Haciendo Buscar un patrón (por 32bytes) en Cuerda (206908949bytes) como uno de línea ... Skip-Rendimiento (más grande-la-mejor): 3041%, 6801754 salta / iteraciones Railgun_Quadruplet_7Hasherezade_hits / Railgun_Quadruplet_7Hasherezade_clocks: 0/58 Railgun_Quadruplet_7Hasherezade rendimiento: 3483KB / reloj

Haciendo la búsqueda del patrón (32bytes) en la cadena (206908949bytes) como una línea ... Saltar-rendimiento (más grande-mejor): 1554%, 13307181 saltos / iteraciones Boyer_Moore_Flensburg_hits / Boyer_Moore_Flensburg_clocks: 0/83 Boyer_Moore_Flensburg performance: 0/83 / reloj

Haciendo una búsqueda de patrón (32bytes) en cadena (206908949bytes) como una línea ... Saltar rendimiento (mayor-mejor): 129%, 160239051 saltos / iteraciones Two-Way_hits / Two-Way_clocks: 0/816 Two -Rendimiento del camino : 247 KB / reloj

Sanmayce,
Saludos


3

El Algoritmo de dos vías que mencionas en tu pregunta (¡que por cierto es increíble!) Se ha mejorado recientemente para que funcione de manera eficiente en palabras de varios bytes a la vez: Coincidencia óptima de cadenas empaquetadas .

No he leído todo el documento, pero parece que confían en un par de nuevas instrucciones especiales de CPU (incluidas, por ejemplo, en SSE 4.2) que son O (1) para su reclamo de complejidad temporal, aunque si no están disponibles pueden simúlelos en tiempo O (log log w) para palabras w-bit que no suenan tan mal.


3

Podría implementar, por ejemplo, 4 algoritmos diferentes. Cada M minutos (para determinar empíricamente) ejecuta los 4 en datos reales actuales. Acumula estadísticas sobre N carreras (también TBD). Luego use solo el ganador para los próximos M minutos.

Registre las estadísticas de las ganancias para que pueda reemplazar los algoritmos que nunca ganan con otros nuevos. Concentre los esfuerzos de optimización en la rutina más ganadora. Preste especial atención a las estadísticas después de cualquier cambio en el hardware, la base de datos o la fuente de datos. Incluya esa información en el registro de estadísticas si es posible, para que no tenga que averiguarlo a partir de la fecha / hora del registro.


3

Recientemente descubrí una buena herramienta para medir el rendimiento de los diversos algos disponibles: http://www.dmi.unict.it/~faro/smart/index.php

Tu podrias encontrar esto útil. Además, si tengo que responder rápidamente al algoritmo de búsqueda de subcadenas, iría con Knuth-Morris-Pratt.


Gracias por el enlace. Las pruebas parecen interesantes para el momento típico del caso, pero no para detectar el peor de los casos.
R .. GitHub DEJA DE AYUDAR A ICE

2

También es posible que desee tener diversos puntos de referencia con varios tipos de cadenas, ya que esto puede tener un gran impacto en el rendimiento. Los algos realizarán diferencias basadas en la búsqueda del lenguaje natural (e incluso aquí aún puede haber distinciones de grano fino debido a las diferentes morfologías), cadenas de ADN o cadenas aleatorias, etc.

El tamaño del alfabeto jugará un papel en muchos algos, al igual que el tamaño de la aguja. Por ejemplo, Horspool es bueno en el texto en inglés pero malo en el ADN debido al diferente tamaño del alfabeto, lo que dificulta la regla de mal carácter. La introducción del buen sufijo lo alivia enormemente.


0

No sé si es lo mejor, pero he tenido una buena experiencia con Boyer-Moore .


¿Conoces una forma de combinar la tabla de cambios malos de Boyer-Moore con Two-Way? Glibc hace una variante de esto para agujas largas (> 32 bytes) pero solo verifica el último byte. El problema es que Two-Way necesita buscar la parte derecha de la aguja de izquierda a derecha, mientras que el mal desplazamiento de Boyer-Moore es más eficiente cuando se busca de derecha a izquierda. Intenté usarlo de izquierda a derecha en bidireccional (avance por la tabla de cambios o desajuste de la mitad derecha normal bidireccional, lo que sea más largo) pero obtuve una desaceleración del 5-10% en comparación con la bidireccional normal en la mayoría de los casos y No se encontraron casos en los que mejorara el rendimiento.
R .. GitHub DEJA DE AYUDAR AL HIELO

0

Esto no responde directamente a la pregunta, pero si el texto es muy grande, ¿qué hay de dividirlo en secciones superpuestas (superposición por la longitud de un patrón), luego busca simultáneamente las secciones usando hilos. Con respecto al algoritmo más rápido, Boyer-Moore-Horspool creo que es uno de los más rápidos, si no el más rápido, entre las variantes de Boyer-Moore. Publiqué un par de variantes de Boyer-Moore (no sé su nombre) en este tema Algoritmo más rápido que Búsqueda de BMH (Boyer – Moore – Horspool) .


0

El más rápido es actualmente EPSM, de S. Faro y OM Kulekci. Ver http://www.dmi.unict.it/~faro/smart/algorithms.php?algorithm=EPSM&code=epsm

"Coincidencia exacta de cadenas empaquetadas" optimizado para SIMD SSE4.2 (x86_64 y aarch64). Se realiza estable y mejor en todos los tamaños.

El sitio al que lo vinculé compara 199 algoritmos de búsqueda rápida de cadenas, y los habituales (BM, KMP, BMH) son bastante lentos. EPSM supera a todos los demás mencionados aquí en estas plataformas. También es lo último.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.