前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Lucene系列(二)int的变长存储与zigzag编码

Lucene系列(二)int的变长存储与zigzag编码

作者头像
呼延十
发布2021-01-24 22:10:24
9690
发布2021-01-24 22:10:24
举报
文章被收录于专栏:呼延呼延

前言

lucene代码量还是比较多的, 在没有看的很明白的情况下, 先写一写新学到的工具类的一些操作吧~也是收获很多.

在lucene写入索引文件时, 为了节省空间,经常会对数据进行一些压缩, 这篇文章介绍一种对int, long类型有用的压缩方式. 即变长存储.

它在lucene中的应用十分广泛, 有事没事就用一下,因此为了熟练的理解代码, 我们还是来一探究竟吧~

在lucene8.7.0版本的代码中, 它没有单独定义成类, 可能是因为是一个小的功能点吧~

对变长数据的写入实现在org.apache.lucene.store.DataOutput#writeVInt中, 对变长数据的读取实现在org.apache.lucene.store.DataInput#readVInt.

定义

什么叫做变长存储? 我们以writeVInt为例,看看注释:

Writes an int in a variable-length format. Writes between one and five bytes. Smaller values take fewer bytes. Negative numbers are supported, but should be avoided. VByte is a variable-length format for positive integers is defined where the high-order bit of each byte indicates whether more bytes remain to be read. The low-order seven bits are appended as increasingly more significant bits in the resulting integer value. Thus values from zero to 127 may be stored in a single byte, values from 128 to 16,383 may be stored in two bytes, and so on.

简单翻译一下:

以可变长度格式写入一个整数. 写入1-5个字节. 越小的值占用的字节越少. 支持负数但是尽量别用. VByte是正整数的变长格式, 每个byte的高位用来标识是否还有更多的字节需要读取. 低位的7个bit位代表实际的数据. 将逐渐读取到的低位附加作为越来越高的高位,就可以拿到原来的整数.

0~127只需要一个字节, 128~16383需要两个字节, 以此类推.

从这里看到,变长整数存储的压缩率,是和数字大小有关系的,数字越小,压缩率越高, 如果全是最大的int, 反而需要更多的字节来存储.

实现

我们实现一个简单的工具类, 能实现上述的变长存储(lucene代码copy出来),之外提供一些辅助我们看源码的方法.

代码语言:javascript
复制
public class VariableInt {

    /**
     * transfer int to byte[] use variable format
     */
    public static byte[] writeVInt(int i) {
        int bytesRequired = bytesRequired(i);
        byte[] res = new byte[bytesRequired];
        int idx =0;
        while ((i & ~0x7F) != 0) {
            res[idx++] = ((byte) ((i & 0x7F) | 0x80));
            i >>>= 7;
        }
        res[idx] = (byte) i;
        return res;
    }


    /**
     * transfer byte[] to int use variable format
     */
    public static int readVInt(byte [] vs) throws IOException {
        int idx = 0;
        byte b = vs[idx++];
        // 大于0,说明第一位为0,说明后续没有数据需要读取
        if (b >= 0) return b;
        int i = b & 0x7F;
        b = vs[idx++];
        i |= (b & 0x7F) << 7;
        if (b >= 0) return i;
        b = vs[idx++];
        i |= (b & 0x7F) << 14;
        if (b >= 0) return i;
        b = vs[idx++];
        i |= (b & 0x7F) << 21;
        if (b >= 0) return i;
        b = vs[idx];
        // Warning: the next ands use 0x0F / 0xF0 - beware copy/paste errors:
        i |= (b & 0x0F) << 28;
        if ((b & 0xF0) == 0) return i;
        throw new IOException("Invalid vInt detected (too many bits)");
    }

    /**
     * compute int need bytes.
     */
    public static int bytesRequired(int i) {
        if (i < 0) throw new RuntimeException("I Don't Like Negative.");
        if ((i >>> 7) == 0) return 1;
        if ((i >>> 14) == 0) return 2;
        if ((i >>> 21) == 0) return 3;
        if ((i >>> 28) == 0) return 4;
        return 5;
    }
}

除了读取写入意外, 提供了一个计算int数字需要几个byte来存储的方法.在我们debug源码时,可以帮助我们分析写入的索引文件.

VariableLong的代码就不贴了.和Variable基本相同,只是变长的长度从1-5变成了1-9而已.

zigzag编码

在Lucene实现的DataOutPut中, 我们可以看到writeZint(int i)方法,经过了解,它使用zigzag编码+变长存储来存储一个整数.

什么是zigzag编码?

首先我们回顾一下计算机编码:

  • 原码:最高位为符号位,剩余位表示绝对值;
  • 反码:除符号位外,对原码剩余位依次取反;
  • 补码:对于正数,补码为其自身;对于负数,除符号位外对原码剩余位依次取反然后+1。

为了方便及其他问题,计算机使用补码来存储整数.

那么我们的变长整数就有一个问题. 他对于负数很不友好.

  • 1这个int整数, 本身存储使用4个字节, 通过上文的变长编码,使用一个字节即可.
  • -1这个int整数,他的补码为: 11111111111111111111111111111111, 也就是说全部是1. 你这时候用变长编码来存储, 需要5个字节, 压缩的目的达不到了.反而多占了空间.

那么基于一个共识: 小整数用的多,因此需要变长编码. 小的负整数也不少,变长编码会压缩率不高甚至反向压缩.

因此诞生了zigzag编码, 它可以有效的处理负数. 它的底层逻辑是: 按绝对值升序排列,将整数hash成递增的32位bit流,其hash函数为h(n) = (n ??1) ^ (n?? 31),

hash函数的作用如图所示:

2021-01-24-02-28-35
2021-01-24-02-28-35

设想一下这个hash函数做了什么?

对于小的负整数而言:

  1. 左移1位可以消去符号位,低位补0
  2. 有符号右移31位将符号位移动到最低位,负数高位补1,正数高位补0
  3. 按位异或 对于正数来说,最低位符号位为0,其他位不变 对于负数,最低位符号位为1,其他位按位取反

那么-1的表示变成了00000000000000000000000000000001, 比较小, 适合使用变长编码了. 1的表示变成了00000000000000000000000000000010, 虽然增大了一点,但是仍然很小,也适合使用变长编码了.

总结一下:

zigzag编码解决了使用变长编码时小的负整数压缩率太低的问题, 它基于一个共识,就是我们使用的小整数(包括正整数和负整数) 是比较多的. 因此将负整数映射到正整数这边来操作.

对应表是:

整数

zigzag

0

0

-1

1

1

2

-2

3

2

4

-3

5

3

6

zigzag实现

这个zigzag的实现比较简单, 在上面已经实现了变长编码的基础上. 只需要实现一个简单的hash函数就好了.

代码语言:javascript
复制
    /**
     * transfer int to byte[] use zig-zag-variable format
     */
    public static byte[] writeZInt(int i) {
        // zigzag 编码
        i = (i >> 31) ^ (i << 1);
        return writeVInt(i);
    }

    /**
     * transfer byte[] to int use zig-zag-variable format
     */
    public static int readZInt(byte[] vs) throws IOException {
        int i = readVInt(vs);
        return ((i >>> 1) ^ -(i & 1));
    }

完美.

总结

本文简单介绍了.

  1. 使用变长编码来对整数进行压缩,对于小正整数能取得不错的压缩率.
  2. 使用zigzag编码对整数进行编码,可以解决掉变长编码对于小负整数压缩率低的难点.

因此, 当你确认你的待压缩数字, 都是比较小的正负整数, 就使用zigzag+变长编码来进行压缩吧, 压缩率25~50%还是可以做到的.

很多需要序列化的开源程序, 都是用zigzag+变长编码来进行整数的压缩, 比如google的protobuf, apache的avro项目, apache的lucene项目, 都在一些场景使用了这套连招, 快快使用吧~.

完。


本文参与?腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客?前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与?腾讯云自媒体分享计划? ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 定义
  • 实现
  • zigzag编码
  • zigzag实现
  • 总结
相关产品与服务
文件存储
文件存储(Cloud File Storage,CFS)为您提供安全可靠、可扩展的共享文件存储服务。文件存储可与腾讯云服务器、容器服务、批量计算等服务搭配使用,为多个计算节点提供容量和性能可弹性扩展的高性能共享存储。腾讯云文件存储的管理界面简单、易使用,可实现对现有应用的无缝集成;按实际用量付费,为您节约成本,简化 IT 运维工作。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
http://www.vxiaotou.com