Tabla de contenido
Introducción a Roaring64Bitmap
Estructura y principio de datos
Estrategia de optimización de RoaringBitmap para Container
Prueba 2: restaurar iterativamente los datos originales que requiere mucho tiempo
Prueba 3: La prueba contiene, y, y No, o método
Introducción a Roaring64Bitmap
Es una versión optimizada de mapa de bits, que ajusta dinámicamente la memoria de acuerdo con los elementos insertados, no ocupa una gran cantidad de memoria debido a menos datos y hará la compresión adecuada.
Estructura y principio de datos
Roaring64Bitmap
El método se basa en la estructura de datos ART para almacenar pares clave / valor. La clave está compuesta por el elemento más alto de 48 bits, y el valor es el contenedor Roaring de 16 bits.
La clase LongUtils dividirá el entero largo de 64 bits (largo) en dos partes de 48 bits altos y 16 bits bajos para su procesamiento.
public static byte[] highPart(long num) {
byte[] high48 = new byte[]{(byte)((int)(num >>> 56 & 255L)), (byte)((int)(num >>> 48 & 255L)), (byte)((int)(num >>> 40 & 255L)), (byte)((int)(num >>> 32 & 255L)), (byte)((int)(num >>> 24 & 255L)), (byte)((int)(num >>> 16 & 255L))};
return high48;
}
public static char lowPart(long num) {
return (char)((int)num); // char 在java中是2个字节。java采用unicode,2个字节(16位)来表示一个字符
}
Tres tipos de contenedor
El siguiente es el núcleo de RoaringBitmap, hay tres tipos de contenedores y el contenedor solo necesita procesar los datos bajos de 16 bits.
ArrayContainer
static final int DEFAULT_MAX_SIZE = 4096
short[] content;
La estructura es muy simple, solo hay una short[] content
y el valor de 16 bits se almacena directamente.
short[] content
Mantenga siempre el orden, sea conveniente para usar la búsqueda binaria y no almacenará valores repetidos.
Debido a que este tipo de contenedor almacena datos sin ningún tipo de compresión, solo es adecuado para almacenar una pequeña cantidad de datos.
El espacio ocupado por ArrayContainer tiene una relación lineal con la cantidad de datos almacenados y cada uno short
es de 2 bytes, por lo que el ArrayContainer que almacena N datos ocupa un espacio de aproximadamente 2N
bytes. El almacenamiento de un dato ocupa 2 bytes y el almacenamiento de 4096 datos ocupa 8 kb.
Según el código fuente, el DEFAULT_MAX_SIZE
valor constante es 4096. Cuando la capacidad excede este valor, el Contenedor actual será reemplazado por BitmapContainer.
BitmapContainer
final long[] bitmap;
Este tipo de contenedor se utiliza para long[]
almacenar datos de mapa de bits. Sabemos que cada contenedor procesa datos con forma de 16 bits, es decir, 0 ~ 65535. Por lo tanto, de acuerdo con el principio de mapas de bits, se necesitan 65536 bits para almacenar datos, y cada bit está representado por 1 para existencia y 0 para no -existente. Cada uno long
tiene 64 bits, por lo que se long
necesitan 1024 para proporcionar 65536 bits.
Por lo tanto, cada BitmapContainer se inicializará con una longitud de 1024 cuando se construya long[]
. Esto significa que no importa si solo se almacena 1 dato en un BitmapContainer o si se almacenan 65536 datos, el espacio ocupado es el mismo 8kb.
RunContainer
private short[] valueslength;
int nbrruns = 0;
Ejecutar en RunContainer se refiere al algoritmo de compresión de longitud de ejecución (Run Length Encoding), que tiene un mejor efecto de compresión en datos continuos.
Su principio es que para los números que aparecen continuamente, solo se registran el número inicial y los números posteriores. cual es:
- Para una secuencia de números
11
, se comprimirá a11,0
; - Para una secuencia de números
11,12,13,14,15
, se comprimirá a11,4
; - Para una secuencia de números
11,12,13,14,15,21,22
, se comprimirá a11,4,21,1
;
short[] valueslength
Los datos almacenados en el código fuente son los datos comprimidos.
El rendimiento de este algoritmo de compresión está muy relacionado con la continuidad (compacidad) de los datos. Para 100 datos continuos short
, se puede comprimir de 200 bytes a 4 bytes, pero para 100 completamente discontinuos, se short
invertirá después de la codificación. Will cambiar de 200 bytes a 400 bytes.
Si queremos analizar la capacidad de RunContainer, podemos hacer las siguientes dos suposiciones extremas:
- En el mejor de los casos, es decir, solo hay un dato o solo una serie de números consecutivos, entonces solo se almacenarán 2
short
, ocupando 4 bytes - En el peor de los casos, todos los bits impares (o todos los bits pares) se rellenan en el rango de 0 ~ 65535, y es necesario almacenar 65536 bits
short
, 128 kb
Estrategia de optimización de RoaringBitmap para Container
Al crear:
- Al crear un contenedor que contiene un solo valor, elija ArrayContainer
- Al crear un contenedor que contiene una serie de valores continuos, compare ArrayContainer y RunContainer, y seleccione el que ocupe menos espacio.
Conversión:
Para ArrayContainer:
- Si la capacidad excede 4096 después de insertar el valor, se convertirá automáticamente a BitmapContainer. Por lo tanto, un ArrayContainer con una capacidad superior a 4096 no aparecerá con un uso normal .
- Cuando se llama al método runOptimize (), comparará el espacio ocupado por RunContainer y elegirá si se convierte a RunContainer.
Para BitmapContainer:
- Si la capacidad es tan baja como 4096 después de eliminar un valor, se convertirá automáticamente a ArrayContainer. Por lo tanto, no habrá BitmapContainer con una capacidad menor a 4096 bajo uso normal .
- Cuando se llama al método runOptimize (), comparará el espacio ocupado por RunContainer y elegirá si se convierte a RunContainer.
Para RunContainer:
- La conversión ocurre solo cuando se llama al método runOptimize (), y el espacio ocupado por ArrayContainer y BitmapContainer se comparan respectivamente, y luego se selecciona si convertir o no.
dependencia de maven
<dependency>
<groupId>org.roaringbitmap</groupId>
<artifactId>RoaringBitmap</artifactId>
<version>0.9.0</version>
</dependency>
Prueba 1 : Antes y después de la optimización, serialización y deserialización que requieren mucho tiempo durante la lectura y escritura de texto
import org.roaringbitmap.RoaringBitmap;
import org.roaringbitmap.longlong.Roaring64Bitmap;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class SerializeToDiskExample {
public static void main(String[] args) throws IOException {
Roaring64Bitmap rb = new Roaring64Bitmap();
for (long k = 0; k < 100000000; k++) {
rb.add(k);
}
for (long k = 100000000; k < 300000000; k= k + 2) {
rb.add(k);
}
long start = System.currentTimeMillis();
String file1 = "D:\\工作\\bitmapwithoutruns.bin";
try (DataOutputStream out = new DataOutputStream(new FileOutputStream(file1))) {
rb.serialize(out);
}
System.out.println("序列化耗间:" + getCost(start) + "秒");
start = System.currentTimeMillis();
rb.runOptimize();
System.out.println("优化压缩耗间:" + getCost(start) + "秒");
start = System.currentTimeMillis();
String file2 = "D:\\工作\\bitmapwithruns.bin";
try (DataOutputStream out = new DataOutputStream(new FileOutputStream(file2))) {
rb.serialize(out);
}
System.out.println("优化后,序列化耗间:" + getCost(start) + "秒");
// verify
Roaring64Bitmap rbtest = new Roaring64Bitmap();
start = System.currentTimeMillis();
try (DataInputStream in = new DataInputStream(new FileInputStream(file1))) {
rbtest.deserialize(in);
}
System.out.println("读取文件,反序列化耗间:" + getCost(start) + "秒");
start = System.currentTimeMillis();
if(!rbtest.equals(rb)) throw new RuntimeException("bug!");
try (DataInputStream in = new DataInputStream(new FileInputStream(file2))) {
rbtest.deserialize(in);
}
System.out.println("优化后,读取文件,反序列化耗间:" + getCost(start) + "秒");
if(!rbtest.equals(rb)) throw new RuntimeException("bug!");
System.out.println("Serialized bitmaps to "+file1+" and "+file2);
}
public static long getCost(long start) {
return (System.currentTimeMillis() - start) / 1000;
}
}
Resultado de la prueba :
序列化耗间:25秒
优化压缩耗间:0秒
优化后,序列化耗间:13秒
读取文件,反序列化耗间:7秒
优化后,读取文件,反序列化耗间:6秒
El tamaño del archivo generado es el siguiente :
Prueba 2: restaurar iterativamente los datos originales que requiere mucho tiempo
import org.roaringbitmap.longlong.LongIterator;
import org.roaringbitmap.longlong.Roaring64Bitmap;
import java.io.DataInputStream;
import java.io.FileInputStream;
import java.io.IOException;
public class SerializeToDiskExample {
public static void main(String[] args) throws IOException {
String file2 = "D:\\工作\\bitmapwithruns.bin";
Roaring64Bitmap rbtest = new Roaring64Bitmap();
try (DataInputStream in = new DataInputStream(new FileInputStream(file2))) {
rbtest.deserialize(in);
}
long start = System.currentTimeMillis();
int count = 0;
// toArray方法只能处理数据个数不大于int最大值,否则会有问题
// for (long number : rbtest.toArray()) {
// count++;
// }
LongIterator it = rbtest.getLongIterator();
long temp = 0L;
while(it.hasNext()) {
temp = it.next();
count++;
}
System.out.println("迭代还原出原来的数据耗时:" +getCost(start) + "秒,数据个数为:" + count);
}
public static long getCost(long start) {
return (System.currentTimeMillis() - start) / 1000;
}
}
El código del método toArray es el siguiente:
public long[] toArray() {
long cardinality = this.getLongCardinality();
if (cardinality > 2147483647L) {
throw new IllegalStateException("The cardinality does not fit in an array");
} else {
long[] array = new long[(int)cardinality];
int pos = 0;
for(LongIterator it = this.getLongIterator(); it.hasNext(); array[pos++] = it.next()) {
}
return array;
}
}
Resultados de la prueba:
迭代还原出原来的数据耗时:6秒,数据个数为:200000000
Prueba 3: La prueba contiene, y, y No, o método
import org.roaringbitmap.longlong.LongIterator;
import org.roaringbitmap.longlong.Roaring64Bitmap;
import java.io.DataInputStream;
import java.io.FileInputStream;
import java.io.IOException;
public class SerializeToDiskExample {
public static void main(String[] args) throws IOException {
String file2 = "D:\\工作\\bitmapwithruns.bin";
Roaring64Bitmap rbtest = new Roaring64Bitmap();
try (DataInputStream in = new DataInputStream(new FileInputStream(file2))) {
rbtest.deserialize(in);
}
long start = System.currentTimeMillis();
System.out.println("contains耗时为O(1), 是否包含" + rbtest.contains(2L) + ", 耗时为:" + getCostMils(start) + "毫秒");
Roaring64Bitmap rbAndNot = new Roaring64Bitmap();
for (long k = 0; k < 100000000; k++) {
rbAndNot.add(k);
}
start = System.currentTimeMillis();
rbtest.andNot(rbAndNot);
System.out.println("andNot方法的耗时为" + getCostMils(start) + "毫秒, 结果个数为:" + rbtest.getLongCardinality());
Roaring64Bitmap rbOr = new Roaring64Bitmap();
for (long k = 300000000; k < 400000000; k++) {
rbOr.add(k);
}
rbtest.clear();
try (DataInputStream in = new DataInputStream(new FileInputStream(file2))) {
rbtest.deserialize(in);
}
start = System.currentTimeMillis();
rbtest.or(rbOr);
System.out.println("or方法的耗时为" + getCostMils(start) + "毫秒, 结果个数为:" + rbtest.getLongCardinality());
rbtest.clear();
try (DataInputStream in = new DataInputStream(new FileInputStream(file2))) {
rbtest.deserialize(in);
}
Roaring64Bitmap rbAnd = new Roaring64Bitmap();
for (long k = 300000000; k < 400000000; k++) {
rbAnd.add(k);
}
start = System.currentTimeMillis();
rbtest.and(rbAnd);
System.out.println("and方法的耗时为" + getCostMils(start) + "毫秒, 结果个数为:" + rbtest.getLongCardinality());
}
public static long getCost(long start) {
return (System.currentTimeMillis() - start) / 1000;
}
public static long getCostMils(long start) {
return System.currentTimeMillis() - start;
}
}
Resultados de la prueba:
contains耗时为O(1), 是否包含true, 耗时为:1毫秒
andNot方法的耗时为18毫秒, 结果个数为:104055296
or方法的耗时为10毫秒, 结果个数为:300000000
and方法的耗时为2毫秒, 结果个数为:9093632
Se comprueba que los datos obtenidos por los métodos andNot y y no son precisos, y se realiza una prueba comparativa de Roaring64Bitmap y RoaringBitmap
import org.roaringbitmap.RoaringBitmap;
import org.roaringbitmap.longlong.Roaring64Bitmap;
import java.io.IOException;
public class SerializeToDiskExample {
public static void main(String[] args) throws IOException {
Roaring64Bitmap rbAnd = new Roaring64Bitmap();
for (long k = 10000; k < 20000; k++) {
rbAnd.add(k);
}
Roaring64Bitmap rbAnd2 = new Roaring64Bitmap();
for (long k = 15000; k < 20000; k++) {
rbAnd2.add(k);
}
rbAnd.and(rbAnd2);
System.out.println("roaring64Bitmap结果个数为:" + rbAnd.getLongCardinality());
Roaring64Bitmap rbAnd3 = new Roaring64Bitmap();
for (long k = 100000; k < 200000; k++) {
rbAnd3.add(k);
}
Roaring64Bitmap rbAnd4 = new Roaring64Bitmap();
for (long k = 150000; k < 200000; k++) {
rbAnd4.add(k);
}
rbAnd3.and(rbAnd4);
System.out.println("roaring64Bitmap结果个数为:" + rbAnd3.getLongCardinality());
RoaringBitmap rbAnd5 = new RoaringBitmap();
for (int k = 10000; k < 20000; k++) {
rbAnd5.add(k);
}
RoaringBitmap rbAnd6 = new RoaringBitmap();
for (int k = 15000; k < 20000; k++) {
rbAnd6.add(k);
}
rbAnd5.and(rbAnd6);
System.out.println("roaringBitmap结果个数为:" + rbAnd5.getLongCardinality());
RoaringBitmap rbAnd7 = new RoaringBitmap();
for (int k = 100000; k < 200000; k++) {
rbAnd7.add(k);
}
RoaringBitmap rbAnd8 = new RoaringBitmap();
for (int k = 150000; k < 200000; k++) {
rbAnd8.add(k);
}
rbAnd7.and(rbAnd8);
System.out.println("roaringBitmap结果个数为:" + rbAnd7.getLongCardinality());
}
}
Resultado de la prueba :
roaring64Bitmap结果个数为:5000
roaring64Bitmap结果个数为:68928
roaringBitmap结果个数为:5000
roaringBitmap结果个数为:50000
Se puede ver en los resultados que el método add de Roaring64Bitmap no es confiable. Este problema se ha informado en github y se espera que lo solucionen.
Actualice la versión a <version> 0.9.7 </version> para solucionarlo.
referencia
Dirección de github: https://github.com/RoaringBitmap/RoaringBitmap
Introducción al algoritmo: https://cloud.tencent.com/developer/article/1136054