首页 > 编程语言 >LevelDB源码剖析(2) 编码与字符串

LevelDB源码剖析(2) 编码与字符串

时间:2022-11-27 01:55:49浏览次数:60  
标签:编码 LevelDB const 字节 Slice 整数 char 源码 value

1. 背景

编码指的是内存里的整数和字符串放到磁盘上的方式,其主要目的有两个

  1. 对不定长整数以及字符串能够在读取的时候感知到已经读取完了整个值
  2. 最大程度的节省在磁盘上占用的空间

2. 设计

2.1 整数

整数的种类上分为定长整数和不定长整数,而定长整数又分为32位整数和64位整数。
整数在存储方式上可以分为小端(Little Endian)和大端(Big Endian)两种存储方式,小端的低位字节存储在低地址端,高位字节存储在高地址端,LevelDB在这里采用的小端存储方式。

2.1.1 定长整数

对定长整数的存储比较简单,例如存储一个32位的定长整数,就是18位的字节编码放在第一个字节位置,916位的字节编码放到第二个字节位置,以此类推

2.2.2 不定长整数

LevelDB应该是不支持存储任意长度的整数的,在这种背景下理论上所有的整数都可以用定长整数的方式来存储。但是还记得刚才说的吗?要节省在磁盘上占用的空间,不定长整数经常会是100以内的小整数,如果采用这种方式进行存储,每个值就会至少占用4个字节的空间,空间利用率很低。
所以LevelDB中对于采用了不定长整数的编码方式来优化这种小整数带来的磁盘空间浪费,不定长整数的存储原理是将原本一个字节存储8个数据信息的结构转换为使用7个字节来存储信息以及1个标志位来标识当前字节是否已经是这个整数的最后一个字节。读取的时候首先读取第一个字节的低七位作为数据的17位,如果表示为位1,则继续拿下一个字节的低七位作为数据的814位,以此类推,下图是以32位整数作为不定长整数存储的例子。

这么做会导致每个整数最多有1个字节的存储量上升,相比于直接使用定长整数的存储方式,这么做在大多数场景下在空间利用率上是更有优势的。

2.2 字符串

字符串使用了长度前缀编码的方式进行存储,也就是开头使用了32位的变长整数来编码字符串长度,编码长度后面跟着字符串的实际值,这么做的好处是不需要指定特定的结尾符,字符串里可以出现任何字符。
一下图为例,开始使用了1个字节表示字符串的长度为5,后续5个字节分别跟了字符串的内容,总共使用6个字节。

3. 源码解析

3.1 Slice

LevelDB中访问字符串大量的使用了一种叫Slice的对象,对象中包含了指向字符串的指针和字符串的长度。相比于C++里的std::string,Slice不管理内存的分配,无论是初始化还是析构,都需要使用者自身去申请或删除字符串的内存,这就使得Slice的拷贝实际是一种对指针的浅拷贝,相比于string非常轻量,同时多个Slice也可以指向同一个字符串。

class LEVELDB_EXPORT Slice {
 public:
  Slice() : data_(""), size_(0) {}
  Slice(const char* d, size_t n) : data_(d), size_(n) {}
  Slice(const std::string& s) : data_(s.data()), size_(s.size()) {}
  Slice(const char* s) : data_(s), size_(strlen(s)) {}


  Slice(const Slice&) = default; //默认成员变量,也就是指针,所以是浅拷贝
  Slice& operator=(const Slice&) = default;

  const char* data() const { return data_; }
  size_t size() const { return size_; }
  bool empty() const { return size_ == 0; }

  char operator[](size_t n) const {
    assert(n < size());
    return data_[n];
  }

 private:
  const char* data_;
  size_t size_;
};

3.2 EncodeVarint64

Encode开头的函数是对value进行不定长整数的编码,并将值给到以dst指向的地址开头,返回值指向的地址结尾(不含)的这块内存中

char* EncodeVarint32(char* dst, uint32_t value);
char* EncodeVarint64(char* dst, uint64_t value);

这边以EncodeVarint64为例,当数值大于128(7个bit存储的量)的时候,每次将数值的低7位给到ptr并将标识符置为1,然后指针后移一位,当数值小于128的时候,将数值低7位给到ptr之后后移一位直接返回。

char* EncodeVarint64(char* dst, uint64_t v) {
  static const int B = 128;
  uint8_t* ptr = reinterpret_cast<uint8_t*>(dst);
  while (v >= B) {
    *(ptr++) = v | B; //v | B表示给最高位置1,最高位是标识位,置为1表示这并不是最后一个字节
    v >>= 7;
  }
  *(ptr++) = static_cast<uint8_t>(v); //最后一个字节不用给标识位置1了,所以单独拿出来最后处理
  return reinterpret_cast<char*>(ptr);
}

3.3 PutVarint64

这些Put开头的函数都是将数字以追加的形式放入到dst后面,只不过区别是放的是定长整数还是非定长整数还是字符串的长度,这边以相对最复杂的PutVarint64为样例看一下具体代码。

void PutFixed32(std::string* dst, uint32_t value);
void PutFixed64(std::string* dst, uint64_t value);
void PutVarint32(std::string* dst, uint32_t value);
void PutVarint64(std::string* dst, uint64_t value);
void PutLengthPrefixedSlice(std::string* dst, const Slice& value);

PutVarint64首先声明了一块10个字节的内存(不定长的64位整数最多 \(\lceil 64 / 7 \rceil = 10\)个字节),然后将其通过EncodeVarint64得到不定长整数的编码后追加到给定的字符串后面。

void PutVarint64(std::string* dst, uint64_t v) {
  char buf[10];
  char* ptr = EncodeVarint64(buf, v);
  dst->append(buf, ptr - buf);
}

为什么不直接拿string的末尾字符去调用EncodeVarint64?
这个与string的内存管理方式有关,string实际是分配了一个比字符串内存大的内存块,如果字符串不断增大超过了内存块大小,程序就会重新分配一个两倍为原来的新内存块,所以说随着字符串的增大,string对象相邻的内存可能会被占用,只能以调用append的方式往后追加字符串。

3.4 GetVarint64

GetVarint64中的input是包含了长度前缀编码的一个

bool GetVarint32(Slice* input, uint32_t* value);
bool GetVarint64(Slice* input, uint64_t* value);
bool GetLengthPrefixedSlice(Slice* input, Slice* result);
const char* GetVarint32Ptr(const char* p, const char* limit, uint32_t* v);
const char* GetVarint64Ptr(const char* p, const char* limit, uint64_t* v);
const char* GetVarint32PtrFallback(const char* p, const char* limit, uint32_t* value);
const char* GetVarint64Ptr(const char* p, const char* limit, uint64_t* value);

传参p开头的内容是一个包含了前缀长度编码的字符串,GetVarint64Ptr的目的是将字符串长度解析出来并赋值到value里面,并返回除去前缀编码后的字符串开头位置。
其中limit是限制寻找编码长度的位置,防止在不定长整数异常的情况下一路找到未定义的内存中去,一般为(p + 10),因为64位不定长整数的最大字节数为10

const char* GetVarint64Ptr(const char* p, const char* limit, uint64_t* value) {
  uint64_t result = 0;
  //从前往后枚举p开头的字节,每次将字节的低7位赋值到result,直到遇到标志位为0的字节
  for (uint32_t shift = 0; shift <= 63 && p < limit; shift += 7) {
    uint64_t byte = *(reinterpret_cast<const uint8_t*>(p));
    p++;
    if (byte & 128) {
      // More bytes are present
      result |= ((byte & 127) << shift);
    } else {
      result |= (byte << shift);
      *value = result;
      return reinterpret_cast<const char*>(p);
    }
  }
  //如果最终都没遇到标志为为0的字节,表示没有获取到value
  return nullptr;
}

GetVarint64目的是将不定长编码的字符串解析为正常内容的字符串(赋值到input)以及其长度(赋值到value)

bool GetVarint64(Slice* input, uint64_t* value) {
  const char* p = input->data();
  const char* limit = p + input->size(); //设置limit,防止访问到字符串以外的内存
  const char* q = GetVarint64Ptr(p, limit, value);
  if (q == nullptr) {
    return false; //没有成功解析
  } else {
    *input = Slice(q, limit - q); //Slice的赋值是一个浅拷贝,也就是说这里不发生字符串的拷贝
    return true;
  }
}

3.5 不同位整数在代码上的区别

在coding.cc这部分代码里面,针对32位不定长整数和64位不定长整数做了很多编码上的“区别对待”

  • 例如EncodeVarint32就选择了硬编码的方式处理数据范围,相比之下EncodeVarint64使用循环显然也能满足需求。
  • GetVarint32Ptr也对小于128的数字做了单独处理,不仅单开了函数,甚至加了inline,超过128的值会调用GetVarint32PtrFallback(在逻辑上这个函数已经包含了GetVarint32Ptr)

这部分的目的我暂时没有把握,所以特地开了这以part,为了之后返回来解答,当前的猜测是对于32位的不定长整数而言,大多数场景下都是小数字(v < (1 << 7), 这么做可以在实际处理中对这部分数据避免循环,从而提升执行的效率

char* EncodeVarint32(char* dst, uint32_t v) {
  // Operate on characters as unsigneds
  uint8_t* ptr = reinterpret_cast<uint8_t*>(dst);
  static const int B = 128;
  if (v < (1 << 7)) {
    *(ptr++) = v;
  } else if (v < (1 << 14)) {
    *(ptr++) = v | B;
    *(ptr++) = v >> 7;
  } else if (v < (1 << 21)) {
    *(ptr++) = v | B;
    *(ptr++) = (v >> 7) | B;
    *(ptr++) = v >> 14;
  } else if (v < (1 << 28)) {
    *(ptr++) = v | B;
    *(ptr++) = (v >> 7) | B;
    *(ptr++) = (v >> 14) | B;
    *(ptr++) = v >> 21;
  } else {
    *(ptr++) = v | B;
    *(ptr++) = (v >> 7) | B;
    *(ptr++) = (v >> 14) | B;
    *(ptr++) = (v >> 21) | B;
    *(ptr++) = v >> 28;
  }
  return reinterpret_cast<char*>(ptr);
}
inline const char* GetVarint32Ptr(const char* p, const char* limit,
                                  uint32_t* value) {
  if (p < limit) {
    uint32_t result = *(reinterpret_cast<const uint8_t*>(p));
    if ((result & 128) == 0) {
      *value = result;
      return p + 1;
    }
  }
  return GetVarint32PtrFallback(p, limit, value);
}

标签:编码,LevelDB,const,字节,Slice,整数,char,源码,value
From: https://www.cnblogs.com/Hugh-Locke/p/16928854.html

相关文章

  • LevelDb基础原理(1) SSTable
    1.介绍1.1描述SSTable(SortedStringTable)是一个通常放在磁盘上的,排序的字符串表,用来高效存储大量的键值对数据,同时搭配上优化实现IO操作的高吞吐量.1.2背景......
  • easylogging++的那些事(四)源码分析(二)日志记录宏(一)CLOG宏(五)其他相关类
    目录el::base::Writer类el::base::NullWriter类el::LogMessage类el::Logger类isValidId接口flush接口initUnflushedCount接口el::base::MessageBuilder类成员变量成员函数......
  • 7-1 哈夫曼树哈夫曼编码
    输入一组整型权值,构建哈夫曼树,实现哈夫曼编码,并输出带权路径长度。输入格式:第一行输入叶子结点个数,接着依次输入权值。输出格式:输出哈夫曼编码,输出带权路径长度。输......
  • springboot事务源码分析
    1、本次使用springboot框架分析事务源码2、打开spring-boot-autoconfigure查看spring.factories发现关于事务的自动配置包含:DataSourceTransactionManagerAutoConfigurati......
  • k8s源码分析6-kubectl功能和对象总结
    kubectl的职责主要的工作是处理用户提交的东西(包括,命令行参数,yaml文件等)然后其会把用户提交的这些东西组织成一个数据结构体然后把其发送给APIServerkubectl的代......
  • 【图像处理】小波编码图像中伪影和纹理的检测附Matlab代码和报告
    ✅作者简介:热爱科研的Matlab仿真开发者,修心和技术同步精进。......
  • 实验三·bdev原理和源码分析
    任务配置bdev运行环境运行hello_bdev程序并分析源码通过bdev接口写入数据并读取Bdev是在物理设备上的进一步抽象,物理层有NVM、NVMeOF、CEPHRBD等,通过bdev向......
  • 实验四·blobstore原理和源码分析
    实验任务学习Blob基本原理完成hello_blob程序运行修改底层bdev为nvmeBlob构建在bdev之上,是对数据存储的进一步抽象,类似于文件,但是不具备文件POSIX接口,可近似按文件形......
  • easylogging++的那些事(四)源码分析(二)日志记录宏(一)CLOG宏(四)日志信息保存
    目录writer类的输出运算符writer类的流操控符el::base::MessageBuilder类CLOG宏接口调用流程图在上一篇中我们分析完了CLOG宏日志输出的流程,在结尾的时候我们提......
  • 常见编码规范
    1.驼峰式骆驼式命名法(Camel-Case)又称驼峰式命名法,正如它的名称CamelCase所表示的那样,是指混合使用大小写字母来构成变量和函数的名字。小驼峰法:小驼峰式命名规则:firstNam......