2009-08-30 10 views
14

Tengo un problema: necesito una búsqueda de los datos del sistema de archivos con un uso eficiente del espacio basado en el prefijo de la ruta del archivo. Prefija la búsqueda del texto ordenado, en otras palabras. Usa un trie, dices, y pensé lo mismo. El problema es que los intentos no son lo suficientemente eficientes desde el punto de vista espacial, no sin otros trucos.Estructura en memoria eficiente en el espacio para texto ordenado que admite búsquedas de prefijo

que tienen una buena cantidad de datos:

  • sobre 450M en un texto sin formato Unix formato de listado en el disco
  • cerca de 8 millones de líneas
  • predeterminado gzip comprime a 31M
  • bzip2 predeterminado comprime a 21M

No quiero comer en ningún lugar cerca de 450M en memoria. En este punto, me gustaría utilizar algo de alrededor de 100M, ya que hay mucha redundancia en forma de prefijos.

Estoy usando C# para este trabajo, y una implementación directa de un trie todavía requerirá un nodo hoja para cada línea en el archivo. Dado que cada nodo hoja requerirá algún tipo de referencia al fragmento final de texto (32 bits, digamos un índice en una matriz de datos de cadena para minimizar la duplicación de cadena), y la sobrecarga del objeto CLR es de 8 bytes (verificado usando windbg/SOS) , Gastaré> 96,000,000 bytes en gastos generales estructurales sin almacenamiento de texto.

Veamos algunos de los atributos estadísticos de los datos. Cuando rellena en un trie:

  • totales únicas "trozos" de texto alrededor de 1,1 millones de
  • trozos únicos total de aproximadamente 16 millones en el disco en un archivo de texto
  • longitud media porción es de 5,5 caracteres, máximo 136
  • cuando no teniendo en cuenta los duplicados, cerca de 52 millones de caracteres totales en trozos
  • nodos trie internos promedio de cerca de 6,5 niños con un máximo de 44
  • aproximadamente 1.8M nodos interiores.

tasas excesivas de creación de la hoja es de aproximadamente 15%, el exceso de interior creación nodo es 22% - por el exceso de creación, me refiero a hojas y nodos interiores creados durante la construcción trie pero no en el trie final como una proporción de la final número de nodos de cada tipo.

Así es un análisis montón de SOS, que indica dónde se está acostumbrando más memoria:

[MT ]--[Count]----[ Size]-[Class           ] 
03563150  11   1584 System.Collections.Hashtable+bucket[] 
03561630  24   4636 System.Char[] 
03563470  8   6000 System.Byte[] 
00193558  425  74788  Free 
00984ac8 14457  462624 MiniList`1+<GetEnumerator>d__0[[StringTrie+Node]] 
03562b9c  6  11573372 System.Int32[] 
*009835a0 1456066  23297056 StringTrie+InteriorNode 
035576dc  1  46292000 Dictionary`2+Entry[[String],[Int32]][] 
*035341d0 1456085  69730164 System.Object[] 
*03560a00 1747257  80435032 System.String 
*00983a54 8052746  96632952 StringTrie+LeafNode 

El Dictionary<string,int> está siendo utilizado para mapear trozos de cuerda a índices en una List<string>, y pueden ser descartados después de la construcción trie, aunque GC no parece estar eliminándolo (un par de colecciones explícitas se realizaron antes de este vuelco) - !gcroot en SOS no indica ninguna raíz, pero anticipo que un GC posterior podría liberarlo.

MiniList<T> es un reemplazo para List<T> utilizando una (es decir, el crecimiento lineal, O(n^2) rendimiento adición) precisamente de tamaño T[] para evitar el desperdicio de espacio; es un tipo de valor y es usado por InteriorNode para rastrear niños.Este T[] se agrega a la pila System.Object[].

Por lo tanto, si subo los artículos "interesantes" (marcados con *), obtengo unos 270M, que es mejor que el texto sin formato en el disco, pero aún no lo suficientemente cerca de mi objetivo. Me imaginé que objeto .NET cabeza era demasiado, y creó un nuevo trie "delgado", usando sólo las matrices de tipo valor a almacenar datos:

class SlimTrie 
{ 
    byte[] _stringData; // UTF8-encoded, 7-bit-encoded-length prefixed string data 

    // indexed by _interiorChildIndex[n].._interiorChildIndex[n]+_interiorChildCount[n] 
    // Indexes interior_node_index if negative (bitwise complement), 
    // leaf_node_group if positive. 
    int[] _interiorChildren; 

    // The interior_node_index group - all arrays use same index. 
    byte[] _interiorChildCount; 
    int[] _interiorChildIndex; // indexes _interiorChildren 
    int[] _interiorChunk; // indexes _stringData 

    // The leaf_node_index group. 
    int[] _leafNodes; // indexes _stringData 

    // ... 
} 

Esta estructura ha reducido la cantidad de datos a 139m, y sigue siendo un trie eficientemente transitable para operaciones de solo lectura. Y como es tan simple, puedo guardarlo trivialmente en el disco y restaurarlo para evitar el costo de volver a crear el trie todo el tiempo.

¿Alguna sugerencia para estructuras más eficientes para la búsqueda de prefijos que trie? Enfoques alternativos que debería considerar?

+0

¿Qué tipo de uso va a hacer con los datos? Mucho procesamiento o solo algunas búsquedas; ¿Puede darnos una idea de qué compensación entre el almacenamiento y el procesamiento eficiente son aceptables? – Jackson

+0

Básicamente se almacenan en caché las operaciones de búsqueda del sistema de archivos para que no sea necesario consultar el disco físico para obtener todos los archivos en un directorio, todos los archivos recursivamente en un directorio, etc. sin consultar el disco, que invariablemente no en la memoria y de hecho es a través de la red => demasiados viajes de ida y vuelta. La expectativa de rendimiento sería que hacer 150 búsquedas de prefijos (es decir, encontrar todas las líneas con este prefijo) devolver promedio de 100 líneas no debería tomar más de, digamos, 100 ms. Tal como están las cosas, mi enfoque de 'SlimTrie' toma 10 segundos cargar desde el disco y lista 8,000,000 líneas => ~ 18ms. –

+0

Y eso con la optimización desactivada, con 8 segundos encendidos, eso incluye el inicio de la aplicación. 140M no es tan malo, pero considerando la redundancia en estos datos, estoy seguro de que se puede mejorar. –

Respuesta

2

Dado que solo hay 1,1 millones de fragmentos, puede indexar un fragmento utilizando 24 bits en lugar de 32 bits y ahorrar espacio allí.

También podría comprimir los trozos. Tal vez Huffman coding es una buena opción. También probaría la siguiente estrategia: en lugar de usar un carácter como símbolo para codificar, debe codificar las transiciones de caracteres. Entonces, en lugar de ver la probabilidad de que aparezca un personaje, mira la probabilidad de la transición en un Markov chain donde el estado es el carácter actual.

+0

Un árbol de Huffman es lo primero que escribí después de ver los fragmentos en el trie - Estaba pensando en codificar líneas como cadenas de bits, una cadena para cada fragmento, concatenada - pero mientras escribía la lógica de empaquetar bits, Pensé en usar matrices de tipo de valor plano para la codificación trie en su lugar. La implementación de la codificación Huffman de manera correcta y eficiente, y la decodificación en particular, se vuelve bastante tediosa con bastante rapidez. Puedo elegir una copia de seguridad y quizás codificar en función de la frecuencia del personaje. –

+0

Sí, indexar usando menos bits que 32 es algo que he pensado. Otras cosas: los datos de caracteres de 16M se recortan cerca de 24 bits, pero si alineé los datos de caracteres a los límites de las palabras, costando en promedio 0.5 bytes por fragmento, podría usar 24 bits para indexar hasta la posición de 32M, para la mitad del ahorro. Y esa lógica de empaquetado de bits que estaba escribiendo para la codificación de árbol Huffman, puede ser útil para usar menos de un número entero de bytes para almacenar índices. Mi próximo paso será escribir una clase de "arreglo de bitfield". –

+0

Voy a premiar a este con la victoria. Escribí una clase de matriz repleta de bits que puede indexar enteros con o sin signo de ancho de bits constante, y determino el ancho máximo requerido al convertir de mi tiempo de carga variable StringTrie a mi SlimTrie inmutable. Almacenar el SlimTrie en el disco y volver a cargarlo más tarde ahorra tiempo y memoria, evitando que la basura del GC no funcione. ¡Ahora hasta 75 millones! –

0

Idea original: en lugar de una tabla hash trie. Tendría en memoria solo el hash y los datos de cadena, tal vez comprimidos.

¿O puede pagar una página leída? Solo hash y la posición del archivo en la memoria, recupera la "página" con líneas que coinciden con ese hash, presumiblemente un número pequeño de líneas ordenadas, por lo tanto, muy rápido de buscar en caso de colisiones.

+0

Hacer 150 busca leer 100 líneas de cada ubicación, no es tan rápido como se podría desear, así es como lo estaba haciendo antes de adoptar el enfoque trie. Estaba usando un índice de línea en el archivo de texto, es decirun archivo que básicamente contiene una matriz plana de desplazamientos de 32 bits al comienzo de cada línea, con el archivo ordenado. El azar busca más de 450 millones de archivos que te maten. –

+0

Para la idea de la tabla hash, no te entiendo del todo. La búsqueda del prefijo no es una clave de longitud fija, podría ser a/b, a/b/c, a/b/c/d, etc. En el primer trie que creo, no el delgado, ya estoy almacenando datos de caracteres una vez usando índices. –

+0

La idea era codificar todo el prefijo, sin importar por cuánto tiempo. Esto daría como resultado un número que es el índice de una "página", la página contiene todas las líneas que coinciden con ese hash. Por lo tanto, solo hace una lectura lógica, recuperando algunas líneas. [Eso podría ser en realidad un par de lecturas físicas, pero con suerte mucho menos de 150 búsquedas.] Entonces, simplemente descarta cualquier colisión hash que no quieras. – djna

1

puede encontrar un artículo científico conectado a su problema here (citación de los autores: "Los experimentos demuestran que nuestro índice soporta la consulta rápida a una ocupación de espacio que está cerca de la alcanzable mediante la compresión del diccionario de la cadena a través de gzip, bzip o ppmdi. "- pero desafortunadamente el papel es pago solamente). No estoy seguro de cuán difícil es implementar estas ideas. Los autores de este documento tienen un website donde también se pueden encontrar implementaciones (bajo "Colección de índices") de varios algoritmos de índice comprimido .

Si desea continuar con su enfoque, asegúrese de consultar los sitios web sobre Crit-bit trees y Radix tree.

+0

Respuesta limpiada de quien solicitó por él (solo podía vincular a un sitio web) –

+0

Está bien, tengo una suscripción a ACM. Voy a ver esto. –

+0

En realidad, el árbol de raíces, o Patricia trie, es la forma en que estoy almacenando mis datos de trie - solo el almacenamiento de un solo carácter por borde/nodo sería claramente una locura para la orientación espacial. –

Cuestiones relacionadas