2010-08-06 15 views
19

Tengo un requisito para (muy) rápidamente procesar cadenas de un rango limitado, contabilizando sus valores. El archivo de entrada tiene el formato:¿Hay alguna manera de hacer esta búsqueda hash más rápido?

January 7 
March  22 
September 87 
March  36 

y así sucesivamente. Debido a que los anchos de línea son idénticos, simplemente puedo leer en una línea con fread razonablemente rápido, y he desarrollado una función de hash perfecta que funciona, pero quería ver si alguien puede ofrecer algún consejo sobre cómo hacerlo aún más rápido. Voy a perfilar cada sugerencia para ver cómo va.

La función de hash se basa en el nombre del mes para permitir la asignación rápida del valor a un depósito. Ten paciencia conmigo aquí. La primera vez que descubrí el número mínimo de caracteres para un hash perfecta:

January 
February 
March 
April 
May 
June 
July 
August 
September 
October 
November 
December 

Tenga en cuenta que los meses son todos nueve caracteres debido al hecho de que tengo toda la línea de entrada.

Desafortunadamente, no hay una sola columna para marcar un mes único. La columna 1 duplica J, la columna 2 duplica a, la columna 3 duplica r, la columna 4 duplica u y las columnas 5 en adelante duplican <space> (hay otros duplicados, pero uno es suficiente para evitar una clave hash de columna única).

Sin embargo, mediante el uso de la primera y cuarta columna, obtener los valores Ju, Fr, Mc, Ai, M<space>, Je, Jy, Au, St, Oo, Ne y De, que son únicos. No habrá valores inválidos en este archivo, así que no tengo que preocuparme por los cubos incorrectos para los datos de entrada.

Al ver los códigos hexadecimales de los caracteres, descubrí que podía obtener valores únicos bajas con sólo AND con valores estratégicos:

FirstChar Hex Binary  &0x0f 
--------- --- --------- ----- 
    A  x41 0100 0001  1 
    D  x44 0100 0100  4 
    F  x46 0100 0110  6 
    J  x4a 0100 1010  10 
    M  x4d 0100 1101  13 
    N  x4e 0100 1110  14 
    O  x4f 0100 1111  15 
    S  x53 0101 0011  3 

SecondChar Hex Binary  &0x1f 
---------- --- --------- ----- 
<space> x20 0010 0000  0 
    c  x63 0110 0011  3 
    e  x65 0110 0101  5 
    i  x69 0110 1001  9 
    o  x6f 0110 1111  15 
    r  x72 0111 0010  18 
    t  x74 0111 0100  20 
    u  x75 0111 0101  21 
    y  x79 0111 1001  25 

y esto me ha permitido configurar una matriz estática para crear una (con suerte) función hash deslumbrantemente rápido:

#define __ -1 
static unsigned int hash (const char *str) { 
    static unsigned char bucket[] = { 
     // A  S D  F    J   M N O 
     __, __, __, __, __, __, __, __, __, __, __, __, __, 4, __, __, // space 
     __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 
     __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 
     __, __, __, __, __, __, __, __, __, __, __, __, __, 2, __, __, // c 
     __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 
     __, __, __, __, 11, __, __, __, __, __, 5, __, __, __, 10, __, // e 
     __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 
     __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 
     __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 
     __, 3, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // i 
     __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 
     __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 
     __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 
     __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 
     __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 
     __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, 9, // o 
     __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 
     __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 
     __, __, __, __, __, __, 1, __, __, __, __, __, __, __, __, __, // r 
     __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 
     __, __, __, 8, __, __, __, __, __, __, __, __, __, __, __, __, // t 
     __, 7, __, __, __, __, __, __, __, __, 0, __, __, __, __, __, // u 
     __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 
     __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 
     __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 
     __, __, __, __, __, __, __, __, __, __, 6, __, __, __, __, __ // y 
    }; 
    return bucket[((unsigned int)(str[3]&0x1f)<<4)|(str[0]&0xf)]; 
} 

Prueba que con el código:

#include <stdio.h> 
#include <string.h> 

// Hash function here. 

static char *months[] = { 
    "January ", "February ", "March ", "April ", "May  ", "June  ", 
    "July  ", "August ", "September", "October ", "November ", "December " 
}; 

int main (void) { 
    int i; 
    for (i = 0; i < sizeof(months)/sizeof(*months); i++) 
     printf ("%-10s -> %2d\n", months[i], hash(months[i])); 
    return 0; 
} 

muestra que es funcionalmente correcto:

January -> 0 
February -> 1 
March  -> 2 
April  -> 3 
May  -> 4 
June  -> 5 
July  -> 6 
August  -> 7 
September -> 8 
October -> 9 
November -> 10 
December -> 11 

pero quiero saber si se puede hacer más rápido.

¿Alguna sugerencia por ahí? Estoy abierto a cualquier optimización simple o incluso a una reescritura total si hay algo intrínsecamente malo con mi función de hash.


No creo que esto sea tan importante, pero la versión final utilizará EBCDIC. La teoría seguirá en pie, pero la operación AND puede cambiar ligeramente ya que los caracteres tienen diferentes puntos de código. Estaré contento con cualquier ayuda solo en el frente ASCII ya que estoy seguro de que cualquier consejo que se ofrezca se traducirá bien en EBCDIC.

+1

No entiendo por qué necesita una tabla hash cuando tiene un número fijo de teclas, que lógicamente representan 0 - 11. Editar: Creo que ahora entiendo, no importa la pregunta :) – leppie

+0

Para aclarar, no puede simplemente tomar una externa la dirección de la cadena y espero que le dé un índice a una matriz :) – leppie

+0

@leppie, tiene razón, los datos de entrada son las cadenas, mi programa de prueba muestra los números (de matriz) solo para mi depuración. Perdón por eso, lo editaré para eliminar cualquier confusión. – paxdiablo

Respuesta

8

Ésta es la secuencia más pequeña que pude encontrar para EBCDIC-estadounidense:

cuenta con 24 elementos en el cubo y utiliza sólo 2 operaciones para calcular el índice:

static unsigned int hash (const char *str) 
{ 
static unsigned char tab[] = { 
    11, 4,__, 7,__,__, 9, 1, 
    __,__,__,__,__,__,__,__, 
    3, 5, 2,10, 8,__, 0, 6 
}; 
return tab[0x17 & (str[ 1 ] + str[ 2 ])]; 
} 

el segundo mejor, 25 elementos con xor:

static unsigned int hash(const char *str) 
{ 
static unsigned char tab[] = { 
    9,__,__, 7,__,__,11, 1, 
__, 4,__,__,__,__, 3,__, 
__, 5, 8,10, 0,__,__, 6, 2 
}; 
return tab[0x1f & (str[ 1 ]^str[ 2 ])]; 
} 

(En realidad, ficha [] debe ser de 32 entradas mucho aquí, porque 0x1f puede generar un desbordamiento de las entradas incorrectas).


actualización de Pax: De lo que vale la pena, la primera opción funcionó a la perfección para la página de códigos EBCDIC 500:

## Month  str[1] str[2] Lookup 
-- --------- ------ ------ ------ 
0 January a (81) n (95)  0 
1 February e (85) b (82)  1 
2 March  a (81) r (99)  2 
3 April  p (97) r (99)  3 
4 May  a (81) y (a8)  4 
5 June  u (a4) n (95)  5 
6 July  u (a4) l (93)  6 
7 August u (a4) g (87)  7 
8 September e (85) p (97)  8 
9 October c (83) t (a3)  9 
10 November o (96) v (a5)  10 
11 December e (85) c (83)  11 
+0

Tendré que esperar hasta la próxima semana para verificar este, no tengo un mainframe en casa :-) Pero, ya que se ve bien para los primeros personajes, asumiré que has hecho el trabajo sucio correctamente. +1. – paxdiablo

+0

Espero que sí (a menos que haya consultado con iconv, por supuesto, O_O ") –

+0

La primera solución se ve genial. En la segunda, la matriz realmente debe completarse con __ al final para evitar el acceso ilegal a memoria. Ambas implementaciones también son distingue entre mayúsculas y minúsculas y no confíe en el espacio posterior. – x4u

1

Parece lo suficientemente agradable para mí. La pregunta es si la función hash es un cuello de botella suficiente como para justificar los esfuerzos continuos de eliminar una o dos operaciones binarias más simples de ella. Dado que el acceso al archivo parece estar involucrado, lo dudo, sin saber ningún detalle sobre el procesamiento general, por supuesto.

EDIT:

tal vez usted podría ver si encuentra cualquier par de caracteres que se traduce en bits inferiores únicos (4, 5 o 6) cuando se añade:

(str[1] + str[2]) & 0x1f 

Si además no hacer , tal vez una de las otras operaciones & | ^. Si esto no ayuda, quizás usando tres caracteres.

+0

Puede ser que las mejoras de la CPU no sean relevantes en relación con la E/S de archivos. Pero los mainframes son increíblemente eficientes en I/O: ahí es donde extraen otras cajas del agua ya que las CPU, hasta hace poco, no eran realmente tan geniales (aunque sí vienen en múltiples "libros" de 54 cada uno, así que muy bien lo compensa). Se observa su consejo: todas estas opciones se someterán a rigurosas pruebas de rendimiento. – paxdiablo

3

Comenzaría con un perfil detallado de su proceso más grande para asegurarme de que no se está involucrando en una optimización prematura.

Esto se ve bastante rápido, pero si la memoria es realmente barata, sería mejor usar una matriz más uniforme y dejar que la memoria caché haga parte del trabajo. Por ejemplo (y pensando en el manguito aquí), ¿qué pasa si simplemente agrega el short que se encuentra en los dos primeros bytes al short en los próximos dos. Eso incluye tanto el primer como el cuarto caracteres, por lo que supongo que debe producir sus 12 valores distintos, y no implica extracciones de campo de bits que pueden no optimizar bien. Luego, haga que la matriz coincidente bucket[] tenga 64K entradas, de las cuales solo 12 serán afectadas. Si funciona bien, esas 12 entradas terminan ocupando parte de su caché D y ha intercambiado un puñado de operaciones aritméticas por un índice en una matriz almacenada en caché más grande.

Pero haga un perfil antes y después de cualquier confusión sobre tratar de hacer que la aritmética sea más rápida, y no se moleste en optimizar dónde realmente no ahorrará tiempo. (Sé que Pax lo sabe, pero es la advertencia obligatoria adjunta a cualquier discusión de optimización.)

+0

En realidad, mirando hacia atrás en los datos, esa no es una mala idea. El tercer y el cuarto carácter son únicos por sí solos, así que podría usar ese único valor de 16 bits como índice, sin manipulación aritmética _o_ bits. Tendré que probar el tamaño de la tabla de búsqueda más grande contra las instrucciones reducidas para ver cómo se desplaza (las fallas de caché aumentadas pueden compensar menos instrucciones) pero de todos modos +1. – paxdiablo

+0

Necesitarás extraer de todos modos, no hay garantía de que los pantalones cortos estén alineados. Sin embargo, no sé cómo se maneja eso en tu arquitectura. – falstro

+0

@ pax, Sin saber mucho más de lo que quiero saber sobre cualquier sistema que todavía use EBCDIC, no puedo adivinar cómo se desarrollará la dinámica de caché frente a aritmética. Siempre que los valores estén bien espaciados en relación con el tamaño de la línea de caché y la asociatividad, existe la posibilidad ... así que parece que vale la pena intentarlo. – RBerteig

2

Ok, como todos en SO, estoy en todo por la reputación ...; *) Como escribí en los comentarios anteriores , el extremo inferior de las arquitecturas de destino tiene un tamaño de línea de caché de 256 bytes, por lo que podría terminar con la caché en las búsquedas de la tabla (su tabla tiene más de 256 bytes). Un intento de doblar la mesa usando algún truco de bit barato en realidad podría obtener algún rendimiento.

He estado jugando con sus datos. También tiene la opción de columna 2 y 3. Sin embargo, todavía no hemos encontrado la forma de obtener eso por debajo de 8 bits.

... y como siempre, perfil, asegúrese de que es el mejor punto para aplicar esfuerzo, y perfil de nuevo después, asegúrese de que sea más rápido.

... y está leyendo más de una línea a la vez, ¿verdad? Los tamaños de registro fijos son buenos de esa manera, no tiene que buscar separadores (líneas nuevas) y puede leer una gran parte de ellos a la vez.

Se puede reducir el tamaño de la matriz mediante el uso de:

#define __ -1 
static unsigned int hash (const char *str) { 
    static unsigned char alloc_to[] = { 
     // A  S D  F    J   M N O 
     __, __, __, __, __, __, __, __, __, __, __, __, __, 4, __, __, // space 
     __, __, __, __, __, __, __, __, __, __, __, __, __, 2, __, __, // c 
     __, __, __, __, 11, __, __, __, __, __, 5, __, __, __, 10, __, // e 
     __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 
     __, 3, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // i 
     __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 
     __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 
     __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, 9, // o 
     __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 
     __, __, __, __, __, __, 1, __, __, __, __, __, __, __, __, __, // r 
     __, 7, __, 8, __, __, __, __, __, __, 0, __, __, __, __, __, // t/u 
     __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 
     __, __, __, __, __, __, __, __, __, __, 6, __, __, __, __, __ // y 
    }; 
    return alloc_to[((unsigned int)(str[3]&0x1e)<<3)|(str[0]&0xf)]; 
} 

que cambia desde 16-por-26 a 16 por-13.

EDITAR

Si, según lo sugerido por otros puestos, sus cuerdas están alineados, de modo que usted pueda usarlos como pantalones cortos, se puede añadir el primer y segundo corto, xor los dos bytes juntos y usted' Tendré una clave única de 8 bits (bueno, siete, en realidad). Puede valer la pena tu tiempo también. Sin embargo, esto es ASCII, por lo que podría no funcionar en EBCDIC. En ASCII, las claves resultan ser:

6e Jan 
7f Feb 
7b Mar 
6a Apr 
47 May 
62 Jun 
58 Jul 
42 Aug 
1a Sep 
11 Oct 
10 Nov 
6d Dec 
+0

Probablemente voy a buscar colapsar filas en lugar de columnas, ya que el cálculo de la fila ya tiene un AND y un bit shift . Para colapsar columnas, tendría que agregar otra carga de bits. Puedo colapsar cada dos columnas a una usando '(str [3] & 0x1e) << 3' en lugar de' (str [3] y 0x1f) << 4'. Si no te importa, me gustaría poner eso en tu respuesta ya que la minimización de caché fue tu idea. – paxdiablo

+0

@pax; gracias, supongo ... :) No hubiera pensado en el '(s & 0x1e) << 3' aunque – falstro

9

Estoy de acuerdo con los otros que no hay mucho margen de mejora. Todo lo que puedo sugerir es una tabla de búsqueda más pequeña que funcione con el mismo número de operaciones, lo que podría hacer que permanezca más tiempo en la memoria caché de la CPU. Además, no depende del espacio que llena los caracteres al final y funciona con cualquier combinación de mayúsculas y minúsculas. Descubrí que agregar cierta solidez razonable a los posibles cambios en los requisitos suele ser rentable en el futuro, especialmente cuando la implementación está optimizada hasta el punto de que los pequeños cambios ya no son tan fáciles.

#define __ -1 
static unsigned int hash (const char *str) 
{ 
    static unsigned char tab[] = { 
     __, __, 1, 11, __, __, __, __, 7, __, __, __, __, 6, 0, 5, 
     8, __, 2, 3, 9, __, 10, __, __, 4, __, __, __, __, __, __ 
    }; 
    return tab[ ((str[ 1 ] >> 4) & 1) + (str[ 2 ] & 0x1f) ]; 
} 

Esto funciona de forma similar a su idea original, pero con menos espacio en blanco:

Month s[1]   s[2]   s[1].4 s[2].4-0 sum lookup 
----- ------------ ------------ ------ -------- --- ------ 
Jan 61:0110 0001 6e:0110 1110  0  14 14  0 
Feb 65:0110 0101 62:0110 0010  0   2 2  1 
Mar 61:0110 0001 72:0111 0010  0  18 18  2 
Apr 70:0111 0000 72:0111 0010  1  18 19  3 
May 61:0110 0001 79:0111 1001  0  25 25  4 
Jun 75:0111 0101 6e:0110 1110  1  14 15  5 
Jul 75:0111 0101 6c:0110 1100  1  12 13  6 
Aug 75:0111 0101 67:0110 0111  1   7 8  7 
Sep 65:0110 0101 70:0111 0000  0  16 16  8 
Oct 63:0110 0011 74:0111 0100  0  20 20  9 
Nov 6f:0110 1111 76:0111 0110  0  22 22  10 
Dec 65:0110 0101 63:0110 0111  0   3 3  11 
      ^   ^^^^^ 
bits:
+0

Oye, eso es bastante bueno, me dio los mismos resultados que mi mesa más grande. Veo que usas diferentes personajes también. Por supuesto, voy a tener que analizarlo, pero se ve bien solo con enchufarlo en mi código de prueba. – paxdiablo

+1

Ooh, elegante! +1 – falstro

+1

Eso es elegante pero, lo que es más importante desde mi punto de vista, furtivo y deshonesto :-) Agregué mi análisis para completarlo. – paxdiablo

1

En ASCII, si se toma month[0]^month[2]^month[3] continuación, se obtiene un control único, con un valor máximo de 95 (julio), que debería permitirle reducir el tamaño de su mesa un poco (y un valor mínimo de 20 (mayo), por lo que una resta lo hace más pequeño de nuevo).

Lo mismo podría no ser cierto en EBCDIC, pero algo similar podría ser.

+0

Esa no es una mala estrategia. Es menor que el tamaño de la mesa original y aún podría obtener una tabla más pequeña con el costo de una única resta (ya que supongo que también hay un valor mínimo de al menos x20 en ASCII ya que esa es la diferencia de bits superior/inferior). Voy a probarlo en EBCDIC cuando vuelva a trabajar pero de todas formas +1. – paxdiablo

+0

@paxdiablo: En realidad, había una colisión que había omitido, pero al agregar 'mes [2]' según mi respuesta actualizada corrige que, * y * hace que la tabla sea más pequeña para arrancar. – caf

+0

En ASCII, 'mes [2]^mes [4]' es único (no importa si '" Mayo "[4] == 0' o' '' ') –

4

Esta es la prueba de EBDIC CCSID (500), el byte de la tabla 32 (pequeño que el suyo, el mismo tamaño que el de X4U):

#define __ -1 
static unsigned int hash(const char *str) 
{ 
    static unsigned char bucket[] = { 
     __, __, __, __, __, __, 1, 8, 
     __, 7, __, __, __, 3, __, __, 
     11, 6, __, __, 4, __, 2, __, 
     __, 0, __, 5, 9, __, __, 10, 
    } 
    return bucket[(unsigned int)(str[0]|str[3]<<1)&0x1f]; 
} 
1

lo que realmente necesita la asignación entre el hash y el índice de mes para hacer la contando? Puede eliminar una búsqueda si en lugar de devolver el mes, devuelve el hash y lo utiliza para el recuento. En x4u's answer la última línea de la función hash podría parecer

return ((str[ 1 ] >> 4) & 1) + (str[ 2 ] & 0x1f) 

y todavía sería capaz de hacer las sumas, la clasificación de los resultados sólo al final, no dentro del bucle.

Cuestiones relacionadas