浅析Impala中的字典编解码

在Impala+Hive表的大数据查询实践中,Parquet是目前比较流行的列存文件格式,虽然文档上支持多种编码,但实际使用的只有普通编码和字典编码,且字典编码使用较多。Parquet的字典编码key使用的是混合了RLE(Run-length encoding,变长编码)和Bit-Packing的编码(RLE/Bit-Packing Hybrid),RLE编码时会先检测值重复出现了连续的多少次(run length),然后存储值和对应的重复次数,对于大量重复值的场景有较好的效果。
在这里插入图片描述
Bit-Packing编码的思想基于一个假设:一般使用int32或int64的值在大部分情况下用不到其类型支持的最大长度,因此只需要存储其有效长度就够了。在编码过程中,事先划分好一个指定位宽(BIT_WIDTH)的空间,将有效数据的二进制格式通过多次位运算按小端序打包到空间中,从最低有效位开始,每个位宽长度的二进制数据被称为一个有效值(Word),下图举例了位宽为3的情况下,打包0、1、2到一个8位二进制值的过程:
在这里插入图片描述
Impala对于Parquet文件的字典解码也是根据对应算法进行实现的,首先读取原始Parquet数据字典和打包过的值(字典key),然后先将key按Bit-Packing进行解码为有效key,再用有效key去数据字典进行检索,返回读取到的数据,其实现参考了开源的FrameOfReference算法,下面进行分析。

由于数据可能很长,Impala对于数据的解码和读取采用了batch的思路,每次读取的数据分为若干个batch,每个batch规定只包含32个有效值,剩余不足一个batch的数据则单独进行读取。单个batch中32个有效值的读取,通过重复执行以下代码实现:

#define DECODE_VALUE_CALL(ignore1, i, ignore2)               \
  {                                                          \
    uint32_t idx = UnpackValue<BIT_WIDTH, i, true>(in);            \
    uint8_t* out_pos = reinterpret_cast<uint8_t*>(out) + i * stride; \
    DecodeValue(dict, dict_len, idx, reinterpret_cast<OutType*>(out_pos), decode_error); \
  }
  BOOST_PP_REPEAT_FROM_TO(0, 32, DECODE_VALUE_CALL, ignore);

可以注意到,这里使用了boost库中的BOOST_PP_REPEAT_FROM_TO宏来帮助实现代码的循环重复调用,每次按顺序先按Bit-Packing算法解码出字典key,然后按照数据类型的长度来获取字典数据,并将其拷贝到输出数组中。BOOST_PP_REPEAT_FROM_TO宏会在编译预处理时把DECODE_VALUE_CALL展开,变为从i=0到31重复调用32次,对应解码第0到第31个字典key,对于为什么没有采用for循环而是重复调用,在UnpackValue函数的注释中有部分解释,在GCC-4.9.2版本编译器可能无法对循环进行剪支和常量传播优化:

Calls to this must be stamped out manually or with
BOOST_PP_REPEAT_FROM_TO: experimentation revealed that the GCC 4.9.2
optimiser was not able to fully propagate constants and remove
branches when this was called from inside a for loop with constant
bounds with VALUE_IDX changed to a function argument.

首先来聚焦UnpackValue,这里实现了Bit-Packing算法的解码,Impala支持0~64的位宽,,Impala需要先根据位宽和要读取的值确定要读取几个Word,从前面介绍Bit-Packing编码的举例中我们可以知道,编码后的值是从原始数据与位宽’AND’后,进行对应左移或右移,最终所有数据一起进行’OR’后得到的。那么解码只需要将其反其道而行之,先获取到数据起点,然后对其进行左移或右移,再与位宽相关的掩码进行’AND’即可得到原始数据。为此,Impala需要先知道几个关键数据:Word数量,数据起点,数据末尾。
Word数量
Impala规定Bit-Packing编码后的数据长度为无符号32位整数值,那么位宽小于32时,只读取一个Word即可,如果跨多个编码值也可通过位运算获取,位宽超过32时就需要读取连续的2或3个Word才能正确解码。因此根据位宽即可直接得到Word数量,与当前解码第几个值无关。
数据起点
为了解码出正确的值,我们需要知道当前要解码的第i个值位于编码buffer的第几个32位编码,还需要知道在这个32位的编码中,数据的偏移量在哪里(位宽不等于32时),这样才可进行左移或右移运算。通过下标i和位宽,可以确定从当前编码buffer的指针起点开始,我们需要的值在第几个bit,将其除以32并向下取整,可得到在我们需要的值在第几个编码上,两者相减即可得到数据起点,即编码内的bit偏移量。
数据终点
编码buffer的长度在UnpackValue中无法获取,部分解码逻辑会把数据解释为64位无符号整数,如果不加限制,极端情况下可能出现访问越界,因此需要确定能否将数据解释为64位无符号整数。这可以直接通过比较数据起点时的编码内偏移量+64(按uint_64解释)和位宽*32(最大长度)的大小来得到。
知道了这些必要常量后,就可以根据不同情况进行解码了,按照Word=1、2、3来进行:
Word=1
这种情况最简单,直接获取到编码buffer指定位置的数据后,右移偏移量的长度再与掩码相与即可。
Word=2
连续的两个Word也相对比较容易处理,获取到起点的Word后将其解释为uint_64,再进行右移偏移量和掩码与运算即可。
Word=3
这种情况比较特殊,相对少见,需要读取3个Word并不说明要读取96位的数据,而是位宽超过32的情况下,数据起点和终点之间可能跨了一个完整的Word,在已读取了连续两个Word的基础上,需要另外与第三个Word相或来得到最终数据,但由于使用的是小端序存储,所以先需要将第三个Word进行左移,再与前面得到的两个Word进行按位或,即可得到最终结果。

获得字典key后,就可以通过key在字典获取数据,这部分逻辑相对简单,根据给定的数据类型可知字典数据的长度,获取到数据后将其memcpy到输出数组中即可完成字典数据的读取。下面附UnpackValue函数和DecodeValue的具体实现:

template <int BIT_WIDTH, int VALUE_IDX, bool FULL_BATCH>
inline uint64_t ALWAYS_INLINE UnpackValue(const uint8_t* __restrict__ in_buf) {
  if (BIT_WIDTH == 0) return 0;

  constexpr int FIRST_BIT_IDX = VALUE_IDX * BIT_WIDTH;
  constexpr int FIRST_WORD_IDX = FIRST_BIT_IDX / 32;
  constexpr int LAST_BIT_IDX = FIRST_BIT_IDX + BIT_WIDTH;
  constexpr int LAST_WORD_IDX = BitUtil::RoundUpNumi32(LAST_BIT_IDX);
  constexpr int WORDS_TO_READ = LAST_WORD_IDX - FIRST_WORD_IDX;
  
  constexpr int FIRST_BIT_OFFSET = FIRST_BIT_IDX - FIRST_WORD_IDX * 32;
  constexpr uint64_t mask = BIT_WIDTH == 64 ? ~0L : (1UL << BIT_WIDTH) - 1;
  const uint32_t* const in = reinterpret_cast<const uint32_t*>(in_buf);

  constexpr bool CAN_SAFELY_READ_64_BITS = FULL_BATCH
      && FIRST_BIT_IDX - FIRST_BIT_OFFSET + 64 <= BIT_WIDTH * 32;

  constexpr bool READ_32_BITS = WORDS_TO_READ == 1
      && (!CAN_SAFELY_READ_64_BITS || BitUtil::IsPowerOf2(BIT_WIDTH));

  if (READ_32_BITS) {
    uint32_t word = in[FIRST_WORD_IDX];
    word >>= FIRST_BIT_OFFSET < 32 ? FIRST_BIT_OFFSET : 0;
    return word & mask;
  }

  uint64_t word = *reinterpret_cast<const uint64_t*>(in + FIRST_WORD_IDX);
  word >>= FIRST_BIT_OFFSET;

  if (WORDS_TO_READ > 2) {
    constexpr int USEFUL_BITS = FIRST_BIT_OFFSET == 0 ? 0 : 64 - FIRST_BIT_OFFSET;
    uint64_t extra_word = in[FIRST_WORD_IDX + 2];
    word |= extra_word << USEFUL_BITS;
  }

  return word & mask;
}
template <typename OutType>
inline void ALWAYS_INLINE DecodeValue(OutType* __restrict__ dict, int64_t dict_len,
    uint32_t idx, OutType* __restrict__ out_val, bool* __restrict__ decode_error) {
  if (UNLIKELY(idx >= dict_len)) {
    *decode_error = true;
  } else {
    // Use memcpy() because we can't assume sufficient alignment in some cases (e.g.
    // 16 byte decimals).
    memcpy(out_val, &dict[idx], sizeof(OutType));
  }
}

参考文档
https://parquet.apache.org/docs/file-format/data-pages/encodings/
https://github.com/lemire/FrameOfReference

  • 7
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值