首页 > 编程语言 >LevelDB 源码中的 C++ 奇淫技巧

LevelDB 源码中的 C++ 奇淫技巧

时间:2024-11-06 19:49:07浏览次数:6  
标签:LEVELDB data Rep 编译 源码 内存 key 奇淫 LevelDB

LevelDB 整体代码还是比较好懂,没有用很多 C++奇淫技巧。不过还是有部分实现,相当比较少见,比如柔性数组、链接符号导出、Pimpl 类设计等。本文会梳理这里的 C++ 高级技巧,帮助更好地理解 LevelDB 的实现。

柔性数组

在 util/cache.cc 的 LRUHandle 结构体定义中,有一个柔性数组(flexible array member) char key_data[1],用来在 C/C++ 中实现可变长数据结构

struct LRUHandle {
  // ...
  char key_data[1];  // Beginning of key

  Slice key() const {
    assert(next != this);
    return Slice(key_data, key_length);
  }
};

在这个 handle 结构体中,key_data[1]实际上只是一个占位符,真正分配给key_data的空间要比 1 字节大,它由 malloc 时计算的total_size确定。具体到 LevelDB 的实现中,在插入新的缓存条目时,会根据 key 的长度动态分配内存,然后将 key 的内容拷贝到这块内存中。如下代码:

Cache::Handle* LRUCache::Insert(const Slice& key, uint32_t hash, void* value,
                                size_t charge,
                                void (*deleter)(const Slice& key,
                                                void* value)) {
  MutexLock l(&mutex_);
  // 计算好一共需要的内存大小, 注意这里减去 1 是因为 key_data[1] 是一个占位符,本来已经有一个字节了
  LRUHandle* e = reinterpret_cast<LRUHandle*>(malloc(sizeof(LRUHandle) - 1 + key.size()));
  e->value = value;
  // ...
  e->refs = 1;  // for the returned handle.
  // 复制 key 数据到 key_data 中
  std::memcpy(e->key_data, key.data(), key.size());
  // ... 忽略

上面代码在单个 malloc 调用中同时为 LRUHandle 结构体和尾部的 key_data 数组分配连续的内存。避免了为键数据单独分配内存,从而减少了额外的内存分配开销和潜在的内存碎片问题。同时 LRUHandle 的整个数据结构紧凑地存储在一块连续的内存中,提高了空间利用率,还可能改善缓存局部性(cache locality)。如果改为使用 std::vector 或 std::string,将需要为每个 LRUHandle 对象分配两次内存:一次是为LRUHandle对象本身,一次是std::vector或std::string为存储数据动态分配的内存。在一个高性能的数据库实现中,这种内存分配的开销是不容忽视的。

另外,这里结构体尾部的数组长度为 1,还有不少代码中,尾部数组长度为 0 或者直接不写,这两种方法有啥区别吗?其实这两种做法都用于在结构体末尾添加可变长度的数据,char key_data[];是一种更明确的尾部数组声明方式,直接表示数组本身没有分配任何空间,是在C99标准中引入。不过这种声明在某些标准 C++ 版本中并不合法,尽管一些编译器可能作为扩展支持它。在C++中,为了避免兼容性问题,通常推荐使用char key_data[1];,因为在编译器中通常有更好的支持。

这里有一些讨论,也可以看看:What's the need of array with zero elements? 和 One element array in struct 。

链接符号导出

在 include/leveldb 中的很多类,比如 db.h 中的 DB 类, 定义的时候带有一个宏 LEVELDB_EXPORT,如下:

class LEVELDB_EXPORT DB {
 public:
 ...
};

这里宏的定义在 include/leveldb/export.h 中,有许多编译选项分支,为了方便看,下面加了缩进(实际代码没有),如下:

#if !defined(LEVELDB_EXPORT)
    #if defined(LEVELDB_SHARED_LIBRARY)
        #if defined(_WIN32)
            #if defined(LEVELDB_COMPILE_LIBRARY)
            #define LEVELDB_EXPORT __declspec(dllexport)
            #else
            #define LEVELDB_EXPORT __declspec(dllimport)
        #endif  // defined(LEVELDB_COMPILE_LIBRARY)

        #else  // defined(_WIN32)
            #if defined(LEVELDB_COMPILE_LIBRARY)
            #define LEVELDB_EXPORT __attribute__((visibility("default")))
        #else
            #define LEVELDB_EXPORT
        #endif
    #endif  // defined(_WIN32)
    #else  // defined(LEVELDB_SHARED_LIBRARY)
        #define LEVELDB_EXPORT
    #endif
#endif  // !defined(LEVELDB_EXPORT)

我们知道 leveldb 本身不像 mysql、postgres 一样提供数据库服务,它只是一个库,我们可以链接这个库来读写数据。为了将 leveldb 导出为动态链接库,需要控制符号的可见性和链接属性。为了支持跨平台构建,这里根据不同的平台信息来指定不同的属性。

在 Linux 系统上,编译库时如果有定义 LEVELDB_COMPILE_LIBRARY,则会加上 __attribute__((visibility("default"))) 属性。它会将符号的链接可见性设置为默认的,这样其他链接到这个共享库的代码都可以使用这个类。

如果不加这个宏来导出符号有什么问题吗?在 Linux 环境下,所有符号默认都是可见的,这样会导出更多的符号,这不仅会导致库的尺寸增大,还可能与其他库中的符号发生冲突。而隐藏部分不对外公开的符号则可以帮助链接器优化程序,提高加载速度,减少内存占用。此外,通过导出宏,可以显式地控制哪些接口是公共的,哪些是私有的,隐藏实现细节实现良好的封装

在没有定义 LEVELDB_SHARED_LIBRARY 的时候,LEVELDB_EXPORT 宏被定义为空,这意味着当 leveldb  被编译为静态库时,所有原本可能需要特殊导出导入标记的符号都不需要这样的标记了。静态链接的情况下,符号导出对于链接过程不是必需的,因为静态库的代码在编译时会直接被包含到最终的二进制文件中。

Pimpl 类设计

在 LevelDB 的许多类中,都是只有一个指针类型的私有成员变量。比如 include/leveldb/table_builder.h 头文件的 TableBuild 类定义中,有私有成员变量 Rep *rep_,它是一个指向 Rep 结构体的指针:

 private:
  struct Rep;
  Rep* rep_;

然后在 table/table_builder.cc 文件中定义了 Rep 结构体:

struct TableBuilder::Rep {
  Rep(const Options& opt, WritableFile* f)
      : options(opt),
        index_block_options(opt),
        file(f),
// ...

这里为什么不直接在头文件中定义 Rep 结构体呢?其实这里是使用了 Pimpl(Pointer to Implementation) 设计模式,主要有下面几个优点:

  • 二进制兼容(ABI stability)。当 TableBuilder 类库更新时,只要其接口(.h 文件)保持不变,即使实现中 Rep 结构体增加成员,或者更改接口的实现,依赖该库的应用程序只用更新动态库文件,无需重新编译。如果没有做到二进制兼容,比如为公开的类增加一些成员变量,应用程序只更新动态库,不重新编译的话,运行时就会因为对象内存分布不一致,导致程序崩溃。可以参考之前业务遇到的类似问题,Bazel 依赖缺失导致的 C++ 进程 coredump 问题分析。

  • 减少编译依赖。如果 Rep 结构体的定义在头文件中,那么任何对 Rep 结构体的修改都会导致包含了 table_builder.h 的文件重新编译。而将 Rep 结构体的定义放在源文件中,只有 table_builder.cc 需要重新编译。

  • 接口与实现分离。接口(在 .h 文件中定义的公共方法)和实现(在 .cc 文件中定义的 Rep 结构体以及具体实现)是完全分开的。这使得在不更改公共接口的情况下,开发者可以自由地修改实现细节,如添加新的私有成员变量或修改内部逻辑。

为什么使用成员指针后,会有上面的优点呢?这就要从 C++ 对象的内存布局说起,一个类的对象在内存中的布局是连续的,并且直接包含其所有的非静态成员变量。如果成员变量是简单类型(如 int、double 等)或其他类的对象,这些成员将直接嵌入到对象内存布局中。可以参考我之前的文章结合实例深入理解 C++ 对象的内存布局 了解更多内容。

当成员变量是一个指向其他类的指针,该成员在内存中的布局只有一个指针(Impl* pImpl),而不是具体的类对象。这个指针的大小和对齐方式是固定的,与 Impl 中具体包含什么数据无关。因此无论指针对应的类内部实现如何变化(例如增加或移除数据成员、改变成员的类型等),外部类的大小和布局都保持不变,也不会受影响。

在 《Effective C++》中,条款 31 就提到用这种方式来减少编译依赖:

如果使用 object references 或 object pointers 可以完成任务,就不要使用objects。你可以只靠一个类型声明式就定义出指向该类型的 references 和 pointers;但如果定义某类型的 objects,就需要用到该类型的定义式。

当然,软件开发没有银弹,这里的优点需要付出相应的开销,参考 cppreference.com: PImpl:

  • 生命周期管理开销(Runtime Overhead): Pimpl 通常需要在堆上动态分配内存来存储实现对象(Impl 对象)。这种动态分配比在栈上分配对象(通常是更快的分配方式)慢,且涉及到更复杂的内存管理。此外,堆上分配内存,如果没有释放会造成内存泄露。不过就上面例子来说,Rep 在对象构造时分配,并在析构时释放,不会造成内存泄露。

  • 访问开销(Access Overhead): 每次通过 Pimpl 访问私有成员函数或变量时,都需要通过指针间接访问。

  • 空间开销(Space Overhead): 每个使用 Pimpl 的类都会在其对象中增加至少一个指针的空间开销来存储实现的指针。如果实现部分需要访问公共成员,可能还需要额外的指针或者通过参数传递指针。

总的来说,对于基础库来说,Pimpl 是一个很好的设计模式。也可以参考 Is the PIMPL idiom really used in practice? 了解更多讨论。

其他

constexpr

constexpr 指定了用于声明常量表达式的变量或函数。这种声明的目的是告知编译器这个值或函数在编译时是已知的,这允许在编译期间进行更多的优化和检查。

static constexpr int kCacheSize = 1000;

与 const 相比,constexpr 更强调编译期常量,而 const 变量在声明时就被初始化,但它们不一定非得在编译时确定,通常只是表示运行时不可修改。

标签:LEVELDB,data,Rep,编译,源码,内存,key,奇淫,LevelDB
From: https://blog.csdn.net/John_ToStr/article/details/143501870

相关文章

  • 微信阅读小程序的设计与实现+ssm(lw+演示+源码+运行)
    由于APP软件在开发以及运营上面所需成本较高,而用户手机需要安装各种APP软件,因此占用用户过多的手机存储空间,导致用户手机运行缓慢,体验度比较差,进而导致用户会卸载非必要的APP,倒逼管理者必须改变运营策略。随着微信小程序的出现,解决了用户非独立APP不可访问内容的痛点,所以很多AP......
  • 基于卷积神经网络的柑桔病害识别与防治系统,resnet50,mobilenet模型【pytorch框架+pytho
     更多目标检测和图像分类识别项目可看我主页其他文章功能演示:柑橘病害识别与防治系统,卷积神经网络,resnet50,mobilenet【pytorch框架,python源码】_哔哩哔哩_bilibili(一)简介基于卷积神经网络的柑桔病害识别与防治系是在pytorch框架下实现的,这是一个完整的项目,包括代码,数据集,......
  • SpringBoot小小主持人网站7q3we(程序+源码+数据库+调试部署+开发环境)
    本系统(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。系统程序文件列表开题报告内容一、项目背景随着社会对儿童综合素质培养的重视,特别是在语言表达和公众演讲能力方面,小小主持人课程受到越来越多家长和孩子的青睐。为满足这一市场......
  • SpringBoot小区物业管理系统3248a--程序+源码+数据库+调试部署+开发环境
    本系统(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。系统程序文件列表开题报告内容一、研究背景与意义随着城市化进程的加速,小区物业管理面临着越来越复杂和多元的挑战。传统的人工管理方式不仅效率低下,还难以满足业主日益增长的多......
  • SpringBoot线上评分分享平台s7103(程序+源码+数据库+调试部署+开发环境)
    本系统(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。系统程序文件列表开题报告内容一、研究背景随着互联网技术的飞速发展,线上消费和在线评价已成为人们日常生活的重要组成部分。然而,现有的线上评分系统往往局限于特定领域,无法满足......
  • SpringBoot响应式博客的设计与实现5g6a7(程序+源码+数据库+调试部署+开发环境)
    本系统(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。系统程序文件列表开题报告内容一、课题背景随着互联网技术的不断进步,博客作为个人表达、知识分享的重要平台,其用户体验和访问便捷性受到越来越多的关注。随着移动设备的普及,用户......
  • AFL++实战入门与afl-fuzz流程解析(源码流程图)
    简介本项目为模糊测试的零基础教学,适合了解pwn且会使用Linux的gcc、gdb的读者。模糊测试旨在通过向程序投喂数据使其崩溃,从而获取崩溃样本以寻找程序漏洞。本文前半部分介绍AFL++的docker环境配置,帮助读者解决入门时的环境和网络问题;后半部分全面解析afl的模......
  • dify专题-后台源码一
            本章开始对Dify最新版本(v0.10.2)源码进行解读。在Dify项目根目录下有如下几个目录:api、web、docker、docker-legacy、sdks等。        其中api是后台项目目录,核心的业务逻辑、模型调用、接口服务代码都在该目录下。web目录是前台项目目录,前台页面代码......
  • 基于数据可视化的房屋租赁财务管家微信小程序设计和实现(源码+论文+部署)
     目录:目录:博主介绍: 完整视频演示:你应该选择我技术栈介绍:需求分析:系统各功能实现一览:1.注册2.登录部分代码参考: 项目功能分析: 项目论文:源码获取:博主介绍: ......
  • 基于数据可视化的智能房租收付管理微信小程序设计和实现(源码+论文+部署)
     目录:目录:博主介绍: 完整视频演示:你应该选择我技术栈介绍:需求分析:系统各功能实现一览:1.注册2.登录部分代码参考: 项目功能分析: 项目论文:源码获取:博主介绍: ......