探索数据编码:Delta Encoding

news/2025/2/7 23:08:03/文章来源:https://www.cnblogs.com/mcdvuli/p/18703440

写在前面

在解决Doris访问AWS上存储的Parquet文件时,曾碰到过Doris不支持Delta Encoding导致数据读取失败。于是打算整理下跟Delta Encoding相关的知识,为解决连续的整型存储、Timestamp、Date类型存储时的压缩效率问题提供参考。

数据编码指的是从一种数据格式转换成另一种数据格式的过程,用来保证传输时数据格式一致或者一定程度上压缩具备某种规律的数据格式,减少传输开销。而Delta Encoding(翻译为增量编码),对增量数据有很好的压缩作用。

LEB128

LEB128(Little Endian Base 128)是一种变长压缩编码,区别于传统固定字节的int等整数类型,LEB128可将任意大的整数存储在少量字节中。

它与可变长量(Variable-length quantity, VLQ)格式相似,主要的区别是LEB128是小端数,VLQ是大端数,两者都允许对任意长的数字进行编码。

它的无符号版本是ULEB128,解码时也必须知道编码的值是ULEB128还是LEB128。

为什么需要这个编码?

如果我们从字节流中读取一个数字,假如这个数字大于2字节(short类型最大值),小于4字节(int类型最大值),需要花费的存储空间仍然是4个字节。但实际上我们读取的数字在多数情况下,实际占用并没有达到4个字节,比如1024307。所以我们需要用LEB128压缩存储空间。

类型 int 无符号int short 无符号short
最大值 2147483647 4294967295 32767 65535

编码过程

LEB128编码时首先需要把一个数的转为二进制格式,然后每7个bit位划分成一组,也就是一个字节只有7个有效位用来存数据,这样最高位就可以用来表示当前字节后续是否还有数据,解码时可用最高位来判断:0代表解码结束,1代表后续还需继续解码。

假设一个数的LEB128编码为0x05F4(小端数),两个字节拆分为0xF40x05,读到0xF4时发现其二进制最高位是1,代表这个LEB128编码的数据还没读完,读到0x05时,最高位是0,就代表这个编码已经解析完,没有要读的数据了。

ULEB128(Unsigned LEB128)

我们以1024307这个数字举例:

      大端 ---------------------> 小端    1111 1010 0001 0011 0011   --  二进制格式01111 1010 0001 0011 0011   --  补0到21位0111110  1000010  0110011   --  每7位为1组00111110 11000010 10110011   --  每个字节最高位补结束标记,1继续解码,0解码完成0x3E     0xC2     0xB3   --  16进制,这个就是VLQ编码结果(大端数):0x3E C2 B3--  ULEB128结果为小端数:0xB3 C2 3E

1024307的ULEB128编码为0xB3C23E,从用Int类型的4字节存储压缩成3字节存储

ULEB128编码:

void encode_uleb128(uint32_t value, uint8_t *output, int32_t* output_size) {int i = 0;while (value != 0) {output[i] = value & 0x7F; // 111 1111value >>= 7;if (value != 0) {output[i] |= 0x80; // 1000 0000}i++;}*output_size = i;
}

ULEB128解码:

uint32_t decode_uleb128(const uint8_t *input) {uint32_t result = 0;int shift = 0;for (int i = 0; ; i++) {result |= (input[i] & 0x7F) << shift;if ((input[i] & 0x80) == 0) {return result;}shift += 7;}
}

LEB128

与ULEB128的区别仅在于对负数的处理:

以-666为例:

      大端 ---------------------> 小端10 1001 1010   --  二进制格式00 0010 1001 1010   --  补0到14位11 1101 0110 0101   --  负数反码11 1101 0110 0110   --  反码+1(负数补码)1111010  1100110   --  每7位为1组01111010 11100110   --  每个字节最高位补结束标记,1继续解码,0解码完成0x7A     0xE6   --  16进制,VLQ编码结果(大端数):0x7A E6--  LEB128结果为小端数:0xE6 7A

上面的例子看着似乎没什么问题,但实际上对有符号int的LEB128编码是有问题的。

十六进制-666的有符号int补码表示:0xFFFF FD66,它会把int类型的32位全部占满(并不是上述例子中只占14位),为了凑满7的倍数,补到35位,加上每一个字节高位的结束标记,导致LEB128编码的结果实际上占40位,共5个字节,因此实际上负数完全没能被压缩

Parquet文件格式对有符号数的处理,是使用ZigZag编码(也称作之字编码)代替补码运算,把有符号的正负数统一转成无符号数,再使用ULEB128编码,这样就能解决上述问题。

ZigZag编码

一种有符号数编码方案,常用于Protocal Buffers中,原理是给正负数交替计数,可以把有符号的正负数映射成无符号数中的正数,可以解决负数压缩效率低的问题。

ZigZag编码很简单,直接通过移位运算实现:zigzag=(n << 1) ^ (n >> k - 1),k代表位数。比如int类型的k=32。

zigzag函数可以把有符号正数放到无符号数的偶数位,扩展出的奇数位就能用来存负数了。使用异或运算能把负数高位的1置为0。

以32位int类型为例:zigzag=(n << 1) ^ (n >> 31)

n hex zigzag(hex)
0 00 00 00 00 00 00 00 00
-1 FF FF FF FF 00 00 00 01
1 00 00 00 01 00 00 00 02
-2 FF FF FF FE 00 00 00 03
2 00 00 00 02 00 00 00 04
-3 FF FF FF FD 00 00 00 05
...

以上表格可以看出,负数转为正数后,高位都变成0,使用ULEB128就能统一对正负数的高位0进行压缩了。

uint32_t zigzag_encode(const int32_t& n) {return n << 1 ^ n >> 31;
}int32_t zigzag_decode(const uint32_t& n) {return static_cast<int32_t>(n >> 1 ^ -(n & 1)); // -(n & 1) = ~(n & 1) + 1)
}

Delta Encoding(增量编码)

在Parquet中,一般用于编码int、timestamp、date等可以存在增量的类型,这些类型在Parquet格式里对应的物理类型一般是INT32或INT64。

格式

将一组数据编码成一个Header和多个Block的变长数组:

image

其中,每个Block又是由多个mini block组成。

一个delta 编码的header需要记录每个block大小、每个block由多少个miniblock组成、存多少个值、第一个数的值。

image

  • 只有the first value(有符号数)使用ZigZag + ULEB128编码,其他三个属性(无符号数)使用 ULEB128编码
  • block size是128的倍数,一个block不一定存满,因此读取完每个block需要跳过末尾的padding,保证block对齐。
  • mini block的大小(block size / num of miniblocks per block)必须是32的倍数,假设block size=128,num of miniblocks=4,那么每个miniblocks大小就是128/4 = 32 bit。

Block

image

  • min delta(有符号数)使用ZigZag ULEB128编码
  • miniblock用来存一组数据的增量,每个mini block的位宽用1字节存储,获取位宽时不足一字节需要对齐。
  • 每个miniblock都是根据它存放在bitwidths的位宽构造成的位序列(bit packed),这里的bitwidths数组可以理解为将miniblock进一步按不同的位宽去划分每一个增量需要的位数。

接着Header中计算miniblock大小的例子:

  • block size=128,4个miniblock,每个miniblock占32位
  • bitwidths数组中存4个位宽:2、4、8、2
  • 每个miniblock存的增量值的个数:16(32/2)、8(32/4)、4(32/8)、16
    • 在miniblock 1中,位宽为2,相当于存16个short的增量值
    • 在miniblock 2中,位宽为4,相当于存8个int型的增量值
  • miniblocks存放每个完成打包的miniblock。

示例

官方的例子假设了block size = 8(实际使用一定是128的倍数)用于演示:

Example 1

7, 5, 3, 1, 2, 3, 4, 5, the deltas would be

-2, -2, -2, 1, 1, 1, 1

The minimum is -2, so the relative deltas are:

0, 0, 0, 3, 3, 3, 3

The encoded data is

header: 8 (block size), 1 (miniblock count), 8 (value count), 7 (first value)

block: -2 (minimum delta), 2 (bitwidth), 00000011111111b (0,0,0,3,3,3,3 packed on 2 bits)


在展示的block中,3的二进制表示11,位宽为2,组成mini block:00 00 00 11 11 11 11,每2个比特位用于存储delta数值,压缩了整型的存储空间。

官方例子里,只有一个位宽为2的miniblock,我们可以假设,后续增量在4-7之间(100-111),位宽为3,那么第二个位宽为3的miniblock里,可以存4、5、6、7这四个增量的值。

在一个block中,根据first value, min delta, delta(根据位宽解码mini block得到)就能够确定存储的数据:

7 (first value)
7 +(-2) + (0) = 5
5 +(-2) + (0) = 3
3 +(-2) + (0) = 1
1 +(-2) + (3) = 2
2 +(-2) + (3) = 3
3 +(-2) + (3) = 4
4 +(-2) + (3) = 5

该例子中,数值都比较小,对于timestamp这类大数值类型来说,只存增量让它的压缩效率非常高。

与RLE/bit packing编码相比,delta encoding每个mini block的位宽可以是不同的,而RLE在整个Page使用统一的位宽。

另外,如果一个block里头的delta增量都是相同值,位宽就是0,只作为标头:

Example 2

1, 2, 3, 4, 5

After step 1), we compute the deltas as:

1, 1, 1, 1

The minimum delta is 1 and after step 2, the relative deltas become:

0, 0, 0, 0

The final encoded data is:

header: 8 (block size), 1 (miniblock count), 5 (value count), 1 (first value)

block: 1 (minimum delta), 0 (bitwidth), (no data needed for bitwidth 0)


从以上示例,可以得到delta编码的步骤:

  • 生成Header
    • 根据数据量,选择合适的block size和每个block的miniblock数目,ULEB128编码后写入Header
    • 总的元素值的个数ULEB128编码后写入Header
    • 第一个值,ZigZag + ULEB128编码后写入Header
  • 写入Block
    • 计算各元素间的差值,作为增量delta,找到最小增量min delta,ZigZag + ULEB128编码后写入Block。
    • 使用min delta作为参照,计算block中所有delta增量减去min delta的值,得到miniblock的每一个delta。
    • 根据block size/miniblock数目,以及delta的大小,划分miniblock,计算位宽数组bitwidths,直接写入Block。
    • 每个minblock的delta根据位宽打包,组成minblocks,直接写入Block
    • 写满当前Block,继续写下一个Block,重新计算delta。

特例就是delta增量都是相同值时,block里只有min delta,除了header和min delta,不会有额外的存储空间,这对连续的timestamp类型来说,大大提高了压缩效率。

参考

  1. Parquet Encoding

https://parquet.apache.org/docs/file-format/data-pages/encodings/

  1. 在Doris中的应用

https://github.com/apache/doris/blob/master/be/src/vec/exec/format/parquet/delta_bit_pack_decoder.h

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/880275.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

0207深度学习:构建个性化 ImageNet 数据集的 LeNet 和 MobileNet 实践

2月7日,晚上,19:30~21:00(主讲老师:郑祥)实验内容: 【深度学习】训练常见的卷积神经网络模型 如LeNet和MobileNet,能制作个性化的ImageNet数据集,涉及到MMEdu、EasyTrain等工具。【2/6 19:00】二阶段直播接入和一阶段直播方式一样。接入方式请参考一阶段内容:【2/5…

注解反射之通过Class对象来操作对象的属性和方法

代码如下package com.loubin;import java.lang.annotation.*; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method;public class Main {public static void main(S…

百度网盘的闲时下载卡入口

https://baijiahao.baidu.com/s?id=1820111080013395787&wfr=spider&for=pc 百度网盘的闲时下载卡入口首先,需要明确是,闲时下载卡的使用时间是01:00-09:00。闲时下载卡的使用时间是01:00-09:00其次,是电脑端的入口。1.在下载界面点击“立即提速”点击箭头指示…

2025年夸克网盘1TB免费空间领取教程,轻松扩容你的网盘

今天为大家带来的是2025年夸克网盘1TB免费空间领取教程,轻松扩容你的网盘。大家好呀!这里是专注为大家挖掘各种超值福利的小助手!你是不是也有过这样的烦恼——网盘存储空间不够用,电影、照片、文件放得满满的,完全没有余地?今天我要给大家带来一个超实用的福利,夸克网盘…

Duplicate Cleaner : 这款神器一键干掉重复文件

在如今这个数字时代,我们的电脑里存储着海量的文件。随着时间的推移,重复文件也越来越多,不仅占用宝贵的磁盘空间,还会让文件管理变得一团糟。 今天,就给大家介绍一款能轻松解决这一难题的神器 ——Duplicate Cleaner。Duplicate Cleaner 有普通版和功能更强大的 Pro 版。…

Doggo:一款友好的命令行DNS查询工具

一、基本概述 Doggo是由Karan Sharma使用Go语言开发的现代命令行DNS客户端工具,旨在以简洁、直观的方式输出DNS查询结果。它类似于传统的dig命令,但提供了更为现代化和易读的输出格式。 https://github.com/mr-karan/doggo二、主要特点 1、支持多种协议: Doggo不仅支持传统的…

uniapp 移动端(ios)uview2.0 u-input 插槽问题

这个插槽太奇怪了,非得加上对于的属性才能使用。<u-input class="u-input" prefixIcon="search" suffix-icon="search" placeholder="请输入验证码" type="text" border="surround"color="#fffffff0&quo…

DeepSeek-R1 技术全景解析:从原理到实践的“炼金术配方” ——附多阶段训练流程图与核心误区澄清

字数:约3200字|预计阅读时间:8分钟(调试着R1的API接口,看着控制台瀑布般流淌的思维链日志)此刻我仿佛看到AlphaGo的棋谱在代码世界重生——这是属于推理模型的AlphaZero时刻。 DeepSeek 发布的 V3、R1-Zero、R1 三大模型,代表了一条从通用基座到专用推理的完整技术路径。…

注解反射之获得Class对象

获得Class对象是实现反射的基础,获得Class对象主要有三种方式 下面是具体实例package com.loubin;import java.lang.annotation.*;public class Main {public static void main(String[] args) throws ClassNotFoundException {Class c = User.class;User user = new User();…

注解反射之获得Class对象介绍

啥是Class对象 专业的详细的科学的规范的解释百度就可以获得,这里写能让自己直观理解的介绍吧。当我们运行程序时,系统会将类加载到内存,同时,会给每个类分配一个Class的对象,这个Class的对象拥有关于这个类的一切描述,就好像人的名片一样。每一个类对应一个唯一的Class对…

java面试心得体会

1.背景 大家有没有感觉到现在就算背诵了很多面试八股文,也刷了B站上很多的面试视频,绝大部分的面试题也基本上都能回答上,但是找工作却越来越难了,是因为自己没有学好么,当然不是很多人认为是经济不好,招聘的单位少,其实我个人觉得也不是最主要的原因估计是学习java编程的人太多…

注解反射之自定义注解

自定义注解主要是要掌握四个元注解@Target, @Retention,@Documented,@Inherited,他们的意思分别如下 下面是一个具体的例子,注意注释定义中的 String name()并不是定义一个name方法,而是定义一个name属性,该属性的类型是Stringpackage com.loubin;import java.lang.ann…