2010-11-01 9 views
10

Tengo una necesidad especial y las preocupaciones más importantes son:algoritmo: gigantesco número de matrices de bits muy dispersos, que codificación a usar

  • en memoria
  • muy bajo consumo de memoria
  • velocidad

Aquí está mi "problema": Necesito almacenar, en la memoria, un gran número de matrices de bits muy dispersas. Esos bitsets son "solo anexados" y se deben usar principalmente para intersecciones. En grande, me refiero a arreglos de hasta 200 000 bits.

El rango debe estar entre [0 ... 16 000 000] para cada conjunto de bits.

que corrieron algunos pre-test con "sólo" 10 673 matrices de bits que contiene algunos datos reales que tengo y tiene los siguientes resultados:

1% of the bit arrays ( 106 bit arrays) Hamming weight: at most  1 bit set 
    5% of the bit arrays ( 534 bit arrays) Hamming weight: at most  4 bits set 
10% of the bit arrays (1068 bit arrays) Hamming weight: at most  8 bits set 
15% of the bit arrays (1603 bit arrays) Hamming weight: at most 12 bits set 
20% of the bit arrays (2137 bit arrays) Hamming weight: at most 17 bits set 
25% of the bit arrays (2671 bit arrays) Hamming weight: at most 22 bits set 
30% of the bit arrays (3206 bit arrays) Hamming weight: at most 28 bits set 
35% of the bit arrays (3740 bit arrays) Hamming weight: at most 35 bits set 
40% of the bit arrays (4274 bit arrays) Hamming weight: at most 44 bits set 
45% of the bit arrays (4809 bit arrays) Hamming weight: at most 55 bits set 
50% of the bit arrays (5343 bit arrays) Hamming weight: at most 67 bits set 
55% of the bit arrays (5877 bit arrays) Hamming weight: at most 83 bits set 
60% of the bit arrays (6412 bit arrays) Hamming weight: at most 103 bits set 
65% of the bit arrays (6946 bit arrays) Hamming weight: at most 128 bits set 
70% of the bit arrays (7480 bit arrays) Hamming weight: at most 161 bits set 
75% of the bit arrays (8015 bit arrays) Hamming weight: at most 206 bits set 
80% of the bit arrays (8549 bit arrays) Hamming weight: at most 275 bits set 
85% of the bit arrays (9083 bit arrays) Hamming weight: at most 395 bits set 
90% of the bit arrays (9618 bit arrays) Hamming weight: at most 640 bits set 
95% of the bit arrays (10152 bit arrays) Hamming weight: at most 1453 bits set 
96% of the bit arrays (10259 bit arrays) Hamming weight: at most 1843 bits set 
97% of the bit arrays (10366 bit arrays) Hamming weight: at most 2601 bits set 
98% of the bit arrays (10473 bit arrays) Hamming weight: at most 3544 bits set 
99% of the bit arrays (10580 bit arrays) Hamming weight: at most 4992 bits set 
100% of the bit arrays (10687 bit arrays) Hamming weight: at most 53153 bits set 

visto los números implicados, que obviamente tendrá que utilizar comprimido arreglos de bits y eso no es un problema: debe ser fácil de tratar visto que las matrices de bits son "solo agregar".

Los bits de la matriz de bits que están encendidos están algo agrupados, pero no totalmente. Por lo tanto, tenderá a tener varios bits en la misma área (pero normalmente no uno después de otro, lo que hace que RLE no sea lo suficientemente bueno para los bits que están activados).

Mi pregunta es ¿qué tipo de compresión usar?

Ahora no sé si debería poner mi primer enfoque aquí o en una respuesta a mi propia pregunta.

Básicamente lo que imaginaba un escenario de "peor de los casos", utilizando una codificación muy tonta:

  • 1 bit: Si está activado, los siguientes 5 bits determinan la cantidad de bits necesaria para calcular el 'omisiones', si off, optimización: los siguientes 5 bits determinan cuántos bits se deben tomar literalmente (es decir, 'on' u 'off', sin omitir) [esto solo se cambiará cuando se determine que es más eficiente que la otra representación, por lo que cuando entre, siempre habrá una optimización (tamaño-sabio)]

  • 5 bits: cuántos bits podemos omitir antes de la siguiente bi t en

  • x bits de: omitir

Aquí está un ejemplo: una matriz de bits tiene conjunto de 3 bits, el primer bit de estar en 3 098 137, el segundo en 3 098 141 y la tercera en 3 098 143.

       +-- now we won't skip 
           | 
           |  +-- 3 because we need 3 bits to store "6" (from 3 098 138 to 3 098 143) 
           |  | +--- 3 098 141 is on 
    22 3 098 137    |  3 | +- 3 098 143 is on 
1 10110 1011110100011000011001 0 00011 000101 etc. 

El primer bit dice que vamos a omitir los bits. 5 bits siguientes (siempre 5) indica cuántos bits necesitamos para saber cuántos bits omitiremos 22 bits diciendo omitir a 3 098 137 un bit apagado diciendo ahora que no estamos salteando los bits 5 bits siguientes (siempre 5) cuenta el número de bits leeremos "tal cual" 6 bits de: fuera, fuera, fuera, encendido, apagado, es decir, en 3 098 141 y 3 098 143 están en etc.

visto el increíble la escasez de estas matrices de bits, esto parece bastante eficiente en cuanto a tamaño.

Utilicé esa codificación, tomé los datos de muestra y calculé el peor de los casos (todavía no escribí el algoritmo, prefiero tener algunas entradas): básicamente consideré que no solo la "optimización de tamaño" nunca funcionaría y, también, que los 5 bits siempre se ajustarían a su valor máximo (24 bits), lo que por supuesto no puede suceder.

Lo hice solo para tener una aproximación muy cruda de lo que podría ser el "peor de los peores" casos.

Me sorprendió muy gratamente:

Worst case scenario: 

108 913 290 bits needed for the 10 687 very sparse bit arrays 
12.9 MB (13 295 KB) 

Los que son datos reales y todos los datos son datos similares, sé que, si las cosas se ponen peor, podría guardar mis 200 000 matrices de bits en aproximadamente 240 MB, lo cual está bien.

Estoy bastante seguro de que la codificación real será menos que eso, pero como aún no la he escrito, solo puedo (muy fácilmente) calcular el "peor caso", por lo que solo muestro eso uno.

Cualquier sugerencia/idea sobre cómo hacer esto más eficiente en cuanto al tamaño (recordando que estas son matrices de bits súper dispersas, que habrá cientos de miles, que deben estar en la memoria, y que serán " Añadir solo ")?

Acerca de mi caso 'solo-agregar'

Básicamente tengo una creciente "expansión" (la gama, pero "expansión" es el término real como yo lo entiendo) y una gran cantidad de matrices de bits que tienen algunos conjuntos de bits. Cuando el rango va desde, digamos, 0 a 1 000 000, todas las matrices de bits van de 0 a 1 000 000 a. Cuando el rango crece a 1 000 001, todas las matrices de bits también crecen, todo en un bit. Pero la mayoría de estas matrices de bits tendrán un '0' agregado en su extremo, mientras que aproximadamente de 4 a 8 de las matrices de bits tendrán un '1' agregado en su extremo. Sin embargo, no puedo predecir de antemano cuál de las matrices de bits tendrá un 0 o un 1 anexado.

Así que tengo un montón de matrices de bits que tienen todas del mismo tamaño, que son todas escasas (< 0.5% de sus bits) y que están "creciendo" a medida que aumenta el rango (entonces re todos siempre creciendo al mismo ritmo).


Judy arrays son geniales. Pero leí sobre ellos hace unos años y esas cosas estaban "por encima de mi cabeza". Las matrices de Judy son una lib 20KLOC exclusiva de C y definitivamente no estoy volviendo a implementar eso. Pero son increíbles.

Así que supongo que tengo que añadir que me gustaría que todo esto se mantuviera relativamente simple, lo que no es tan exagerado como la propiedad especial de "solo agregar" de mis matrices de bits muy dispersas.

+0

Tenga en cuenta que los comentarios sobre reinventar la rueda se pueden enviar a */dev/null *: si solo para la matemática/desafío detrás de ella Quiero implementar esto yo mismo. Y de todos modos estaría muy sorprendido de encontrar una rueda que pueda manejar 200 000 arreglos de bits "solo para agregar" en la memoria :) Pero si tienes uno, la mecánica detrás de esto me interesa mucho :) – SyntaxT3rr0r

+1

Hay teoría límite en la densidad de codificación: con un conjunto de N elementos, de los cuales n se establecen, el número mínimo de bits para codificar sería -n * log2 (n/N) - (Nn) * log (1-n/N). Para su matriz en la que se establece 53153 de 16M, esta sería de 514kBits y para 4992 bits configurada - 65 kBits. Y acerque su memoria a este límite, codificación más compleja que debe elegir. – Vovanium

+1

@Vovanium, creo que dejaste fuera un contexto necesario para tu límite teórico (como, algún tipo de suposición estadística sobre la distribución de los bits que se establecen?) – comingstorm

Respuesta

2

Incluso si no son exactamente lo que estás buscando, merece la pena comprobar Judy trees. Judy es una biblioteca altamente optimizada para mapas ordenados, y una configuración está específicamente diseñada como un conjunto de bits en lugar de un mapa. No creo que la intersección sea una de las operaciones optimizadas originalmente, aunque ...

La idea general es usar un árbol con un número fijo de bits de dirección por nivel, y aprovechar la escasez en cada nivel . Esto da como resultado una compresión bastante buena incluso en el peor de los casos, y un rendimiento de consulta rápido también. Creo que una operación de intersección sería relativamente simple y potencialmente muy rápida.

En cualquier caso, siempre es una buena idea robar lo mejor.

+0

yup Los arreglos Judy son geniales, pero la verdad es que los cálculos detrás de esto son un poco demasiado complicados :) Y AFAICT solo está disponible como una letra escrita en 20KLOC C : -/Definitivamente estoy reinventando * esa * rueda :) – SyntaxT3rr0r

+0

Maldición, quise decir, definitivamente * no * estoy reinventando * esa * rueda :) Obviamente :) – SyntaxT3rr0r

+0

No hay necesidad de reinventar su rueda, pero el principio básico parece ser el tipo de cosa que estás buscando: muy escasa y fácilmente adaptable a la escritura de una función de intersección rápida. – comingstorm

1

Teniendo en cuenta que vas a hacer un montón de pruebas de intersección de todos modos, tal vez deberías intentar almacenar todos los bitvectores en paralelo. Una lista de entrada escasa, 16M. Cada entrada en esa lista contiene una lista de cuáles de los 200k bits de entrada tienen un '1' en esa ubicación. Parece que espera tener solo 5 bits establecidos por vector de entrada, o 1M de entradas totales? Tomando una implementación de la lista enlazada de paja para el nivel superior y las cubetas, y el peor caso de ninguna intersección (por lo tanto cubos de 1M con 1 elemento cada uno), podría almacenarlo todo en 32MB.

+0

no no, la lista que publiqué lo muestra, por ejemplo: * "50% de los bitvectores tendrán [entre 55 y] 67 bits configurados "*.Habrá mucho más de 1 millón de entradas totales. Con 200K bitvectores diría que habría, muy groseramente, un total de 100 millones de bits configurados. – SyntaxT3rr0r

+0

No lo vi de esta manera pero ahora que mencionas hacerlo "al revés", se garantiza que cada una de las * "extensiones" * (la gama de 16 millones) se usará varias veces. La forma en que lo redactó, cada entrada en la lista de 16M tendría un conjunto de 4 a 8 bits. – SyntaxT3rr0r

+0

Aha, pensé que era un total, por lo tanto 55k/10k = 5, mi error. Entonces, no hay razón para hacer que el conjunto de 16M sea escaso, cada entrada necesita espacio para unos 8 identificadores de 18 bits (2^18> 200k), por lo que 288MB. Similar a su estimación. –

3

Puede usar un árbol binario para la matriz de bits. Digamos, tienes una matriz con rango de [M..N]. Almacénelo de tal manera:

Elija una codificación numérica para [0 ... tamaño de ram], como Fibonacci, Golomb o Rice code (puede elegir la representación más adecuada después de crear un perfil de su programa con datos reales).

  1. Si matriz está vacía (no tiene bits puestos), almacena como número 0.
  2. Si matriz está llena (se han fijado todos los bits), almacenarla como número 1.
  3. dividir Else en dos partes: A en [M .. (M + N)/2-1] y B en [(M + N) /2..N]
  4. Genera representaciones de P0 y P1 utilizando este algoritmo recursivamente.
  5. Obtenga la longitud de P0 (en bits u otras unidades la longitud puede ser un número entero) y almacénela como un número (puede necesitar agregar 1 si la longitud puede ser 1, por ejemplo, almacena 0 como único bit 0).
  6. Tienda P0 y luego P1.

En este caso, si los límites son comunes, las operaciones de intersección de una unión son recursiones triviales:

Intersección:

  1. Si la matriz A está vacía, almacenar 0.
  2. Si array A está lleno, copia de la tienda de B
  3. Arreglos de la otra partición, hacer intersecciones de ambas mitades, almacenar la longitud de la primera mitad, luego ambas mitades.

Este algoritmo puede tratar con bits (si necesita que sean más compactos) y bytes/palabras (si las operaciones de bits son tan lentas).

También puede agregar codificaciones específicas para matrices con un solo bit establecido, todas las matrices con un tamaño inferior a algún límite (8 elementos, por ejemplo) para disminuir el nivel de recursión.

El inconveniente es que sin algunos hacks la adición o eliminación de elementos a/desde la matriz es una operación compleja (tan compleja como las operaciones de intersección/unión).

Por ejemplo, la matriz con un solo bit 0xAB debe almacenarse en una matriz de 0 ..0xFF como (pseudocódigo para):

códigos
0 1  2 3 4  5 6 7  8 9 10  11 12  13 14  15  16 
1, EMPTY, 13, 1, EMPTY, 9, 1, EMPTY, 5, 1, EMPTY, 1, EMPTY, FULL, EMPTY, EMPTY, EMPTY 
                | AA | AB | 
             |A8..A9| AA .. AB | 
             | A8   ..  AB |AC..AF| 
          |A0..A7| A8    ..    AF | 
         | A0     ..     AF |B0..BF| 
       |80..9F| A0      ..      BF | 
      | 80         ..      BF |C0..FF| 
| 0..7F| 80           ..       FF |  

vacíos y llenos son para las matrices vacías y llenas, los números se longitudes de elementos (deben ser reemplazados con lengthts reales en bytes, bits o menos)

Ff se no necesita verificación rápida de un solo bit, puede usar el enfoque más simple: Simplemente almacene las distancias entre los bits establecidos usando los códigos: fibonacci, rice, golomb, levenshtein, elias etc. o invente otro. Tenga en cuenta que para obtener una longitud de código mínima, debe usar código con longitudes de código lo más cercanas posible a -log p/log 2, donde p es la probabilidad de ese código. Puedes usar el código huffman para eso.

Por ejemplo, el uso Elias código gamma, así array como esto:

0 1 0000 1 1 000 1 0 1 000000000000000000 1 000000000000000000 
2  5 1 4 2   19     18   (distance) 

se codificarán como:

010 00101 1 00100 010 000010011 000010010 
2 5 1 4 2  19  18  (distance code explained) 

Y sobre todo compacto para matriz con distribución bits de uniforme sería codificación aritmética, pero es muy molesto el tiempo de CPU. Debido a que tendrá que leer y escribir dichas matrices poco a poco sin saltos rápidos disponibles.

+0

+1, gran respuesta también. Todavía no sé qué ruta seguiré, pero esto seguro me da algo de comer :) – SyntaxT3rr0r

+0

Gracias. También puedo recomendar mirar cómo hicieron varios algoritmos de compresión de sonido (MP2, AAC, etc.). Se ocupan de matrices dispersas (como 0, 0, 0, 1, 0, -1, 1, 0, 0, 0, 0, 0, 0, 2, 0, 1, 0) cuando se comprimen espectros de alta frecuencia. – Vovanium

4

No dijo qué lenguaje de programación desea usar. Parece que no quieres a Judy porque es "C-only" ... si estás usando C#, entonces podrías usar mi Compact Patricia Trie. Es de casi 4500 LOC (comentada) y utiliza ideas similares a Judy, pero el tamaño y la velocidad de cada trie no son ideales debido a las limitaciones de .NET. No está optimizado para el cálculo de intersecciones, pero tal algoritmo podría agregarse. El artículo sobre CP Tries no enfatiza este punto, pero puede almacenar conjuntos (matrices de bits dispersas) de forma mucho más compacta que los diccionarios (los gráficos en el artículo muestran el tamaño y la velocidad de los diccionarios, no los conjuntos).

El mejor caso es un grupo denso de bits. Con un 50% de ocupación (cada dos bits configurados), requiere menos de 8 bits por clave (menos de 4 bits por entero). (Corrección: menos de 8 bits, no más.)

Si solo necesita una representación aproximada de los datos, use un Bloom filter.

Por cierto, ¿qué quiere decir con "solo agregar"? ¿Significa que solo agrega claves, o que cada clave que agrega es mayor que las claves que agregó antes?

Actualización: Dado que solo está agregando teclas más grandes, probablemente debería diseñar un algoritmo especial para su caso. OMI, al diseñar un algoritmo personalizado, debe hacerlo lo más simple posible. Así que esta es mi idea, que asume que las claves de diferentes bitsets no están correlacionadas (por lo tanto, no hay beneficio de intentar comprimir datos entre diferentes bitsets):

Un conjunto de bits está representado por una matriz ordenada de ranuras de 32 bits. Debido a que está ordenado, puede usar la búsqueda binaria para buscar claves. Cada ranura consiste en un "prefijo" de 24 bits y 8 bits de "banderas". Cada ranura representa una región de 8 teclas. Las "banderas" le indican cuáles de las 8 claves de la región están presentes en el conjunto de bits, y el "prefijo" le dice de qué región estamos hablando al especificar los bits 3 a 26 de la clave. Por ejemplo, si los siguientes bits son "1" en el conjunto de bits:

1, 3, 4, 1094, 8001, 8002, 8007, 8009 

...entonces el bitset está representado por una matriz de 4 ranuras (16 bytes):

Prefix:  0, 136, 1000, 1001 
Flags: 0x15, 0x40, 0x86, 0x02 

La primera ranura representa 1, 3, 4 (aviso de que los bits 1, 3 y 4 están situados en el 0x15 número); la segunda ranura representa 1094 (136 * 8 + 6); la tercera ranura representa 8001, 8002 y 8007; la cuarta ranura representa 8009. ¿Tiene sentido esto?

No sé si esto es tan compacto como su idea. Pero creo que obtendrás consultas más rápidas y modificaciones más rápidas, y será bastante fácil de implementar.

+0

+1, buena respuesta. No sé mucho sobre Patricia Trie todavía (además del nombre que ya escuché), leeré. Sí, por * "solo agregar" * Quiero decir que a medida que crezca la "expansión" (el rango), algunas de las matrices de bits (normalmente de 4 a 8) tendrán un bit establecido al final de la matriz de bits. Así que nunca "inserto" ningún bit en el medio de una matriz de bits. Entonces, realmente es un caso especial que, creo, hace las cosas mucho más fáciles. – SyntaxT3rr0r

+0

Supongo que con "solo agregar" quiero decir tanto que solo agrego claves y que la clave también es siempre mayor que la clave que agregué antes. – SyntaxT3rr0r

+0

Ojalá pudiera dar más de +1, su artículo se ve excelente, también lo hace su implementación C# de "CPT". En realidad, el lenguaje que busco es * probablemente * Java, pero puede que necesite una manera fácil de llevar esto a C# y Objective-C ... Así que prefiero algo relativamente fácil. Pero su compacta Patricia Trie se ve increíble. Una vez más, mi caso es muy especial: la mayoría de mis matrices de bits no tienen ni siquiera el 0,5% de cada bit establecido, por lo que en realidad es * súper disperso *. – SyntaxT3rr0r

1

Puede que le interesen los diagramas de decisión binaria (BDD) y, más precisamente, el diagrama de decisión binaria suprimido por cero (ZBDD).

Se utilizan para representar conjuntos de forma comprimida. A diferencia de otras formas comprimidas, las operaciones (como establecer intersecciones o inserciones de elementos, ¿su cosa de "solo agregar"?) Funcionan directamente en la forma comprimida.

+0

He editado un poco mi pregunta para aclarar la "única cosa anexada". Básicamente, las matrices de bits están creciendo (hasta un máximo de 16 000 000 bits) y siempre estoy modificando el final, por lo que es bastante fácil trabajar directamente en la forma comprimida. – SyntaxT3rr0r

Cuestiones relacionadas