Práctica de mapa de bits de Roaring64

Tabla de contenido

Introducción a Roaring64Bitmap

Estructura y principio de datos

Tres tipos de contenedor

ArrayContainer

BitmapContainer

RunContainer

Estrategia de optimización de RoaringBitmap para Container

dependencia de maven

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 

Prueba 2: restaurar iterativamente los datos originales que requiere mucho tiempo

Prueba 3: La prueba contiene, y, y No, o método

referencia


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

Roaring64BitmapEl 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[] contenty el valor de 16 bits se almacena directamente.

short[] contentMantenga 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 shortes de 2 bytes, por lo que el ArrayContainer que almacena N datos ocupa un espacio de aproximadamente 2Nbytes. 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_SIZEvalor 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 longtiene 64 bits, por lo que se longnecesitan 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á a 11,0;
  • Para una secuencia de números 11,12,13,14,15, se comprimirá a 11,4;
  • Para una secuencia de números 11,12,13,14,15,21,22, se comprimirá a 11,4,21,1;

short[] valueslengthLos 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 shortinvertirá 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

 

Supongo que te gusta

Origin blog.csdn.net/xiao__jia__jia/article/details/113069094
Recomendado
Clasificación