LevelDB, 一份 C++ 学习指南

C++ 太繁琐了,功能太多, Follow LevelDB, 它只使用很少的好的 C++ 特性, 可以当作 C with Class 升级版本.

在基本对象设计方面,LevelDB 对 structclass 的使用遵循清晰的原则。struct 用于纯数据聚合 (POD-like):

struct LEVELDB_EXPORT Options {
  // 默认构造所有字段,纯数据结构不用写太复杂的构造函数
  Options();
  ...
};

OptionsReadOptionsWriteOptions 都使用 struct,因为它们本质上是配置参数的集合,所有成员默认 public,允许自由访问和复制。

struct ManualCompaction {
    int level;
    bool done;
    const InternalKey* begin;  // null = 范围起点
    const InternalKey* end;    // null = 范围终点
    InternalKey tmp_storage;   // 压缩进度记录
  };

内部数据结构如 ManualCompactionCompactionStats 也使用 struct。

class 则用于有行为或不变式的类型:

  DB() = default;

  DB(const DB&) = delete;
  DB& operator=(const DB&) = delete;

  virtual ~DB();

DBCacheIterator 等需要封装、有资源管理需求的类型使用 class。

LevelDB 中大量类禁止复制,这是 C++ 资源管理的核心模式。标准写法 (C++11) 如下:

  Table(const Table&) = delete;
  Table& operator=(const Table&) = delete;

禁止复制的原因主要有三点:一是资源所有权唯一性,如 Arena 管理内存块,PosixSequentialFile 持有文件描述符;二是避免浅拷贝问题,防止多个对象指向同一资源导致 double-free;三是语义不适合复制,如 Env 是全局环境、DB 是数据库句柄。

  Arena(const Arena&) = delete;
  Arena& operator=(const Arena&) = delete;
  MutexLock(const MutexLock&) = delete;
  MutexLock& operator=(const MutexLock&) = delete;

但有些类型应该允许复制:

  // Slice 只是个指针+长度的 wrapper,拷贝一下无妨
  Slice(const Slice&) = default;
  Slice& operator=(const Slice&) = default;

Slice 只是指针+长度的轻量级视图,复制开销小且语义正确。

  // WriteBatch 也是值语义,随便拷
  WriteBatch(const WriteBatch&) = default;
  WriteBatch& operator=(const WriteBatch&) = default;

在析构函数与资源管理方面,LevelDB 严格遵循 RAII 原则:资源在构造时获取,在析构时释放。

Arena::~Arena() {
  for (size_t i = 0; i < blocks_.size(); i++) {
    delete[] blocks_[i];
  }
}

Arena 析构时释放所有分配的内存块。

  PosixSequentialFile(std::string filename, int fd)
      : fd_(fd), filename_(std::move(filename)) {}
  ~PosixSequentialFile() override { close(fd_); }

文件类在析构时关闭文件描述符。

Status 类实现了自定义的复制和移动语义:

  // 默认构造为 OK 状态,无内存分配,高效
  Status() noexcept : state_(nullptr) {}
  ~Status() { delete[] state_; }

  Status(const Status& rhs);
  Status& operator=(const Status& rhs);

  Status(Status&& rhs) noexcept : state_(rhs.state_) { rhs.state_ = nullptr; }
inline Status::Status(const Status& rhs) {
  state_ = (rhs.state_ == nullptr) ? nullptr : CopyState(rhs.state_);
}
inline Status& Status::operator=(const Status& rhs) {
  // 处理了自赋值 (this == &rhs) 
  // 也处理了 rhs 和 *this 都有值的情况 -> 先释放旧的,再拷新的
}
inline Status& Status::operator=(Status&& rhs) noexcept {
  std::swap(state_, rhs.state_);
  return *this;
}

注意 operator= 中检查 state_ != rhs.state_ 同时处理了自赋值和两者都是 OK 的情况。

在接口设计方面,LevelDB 广泛使用抽象基类定义接口,配合工厂模式使用:

class LEVELDB_EXPORT Comparator {
 public:
  virtual ~Comparator();

  // 三路比较: <0, ==0, >0
  // 标准的 C 风格比较返回值
  virtual int Compare(const Slice& a, const Slice& b) const = 0;

通过工厂函数返回具体实现:

// 内置的字节序比较器
// 注意:返回的单例由模块管理,千万别手欠 delete 它
LEVELDB_EXPORT const Comparator* BytewiseComparator();
// 创建固定大小的 LRU Cache
// 实现了标准的最近最少使用算法
LEVELDB_EXPORT Cache* NewLRUCache(size_t capacity);

LevelDB 使用 NoDestructor 实现安全的单例模式:

template <typename InstanceType>
class NoDestructor {
 public:
  template <typename... ConstructorArgTypes>
  explicit NoDestructor(ConstructorArgTypes&&... constructor_args) {
    // static_assert 确保 storage 大小和对齐满足要求 ...
    new (instance_storage_)
        InstanceType(std::forward<ConstructorArgTypes>(constructor_args)...);
  }

  ~NoDestructor() = default;  // 故意不析构,避免静态销毁顺序问题

  InstanceType* get() {
    return reinterpret_cast<InstanceType*>(&instance_storage_);
  }

 private:
  alignas(InstanceType) char instance_storage_[sizeof(InstanceType)];
};

使用示例:

const Comparator* BytewiseComparator() {
  static NoDestructor<BytewiseComparatorImpl> singleton;
  return singleton.get();
}

更复杂的单例实现是 SingletonEnv,思路与 NoDestructor 一致,同样使用 aligned storage + placement new,额外在 Debug 模式下记录初始化状态用于断言检查:

Env* Env::Default() {
  static PosixDefaultEnv env_container;  // SingletonEnv<PosixEnv> 的 wrapper
  return env_container.env();
}

Table 类使用 PIMPL 模式 (Pointer to Implementation),通过前向声明的 Rep 结构隐藏实现细节:

 private:
  struct Rep;                              // 前向声明,头文件不暴露实现细节
  explicit Table(Rep* rep) : rep_(rep) {}
  Rep* const rep_;                         // 所有实现细节藏在 Rep 里

RAII 锁管理是另一个重要的设计模式:

class SCOPED_LOCKABLE MutexLock {
 public:
  explicit MutexLock(port::Mutex* mu) EXCLUSIVE_LOCK_FUNCTION(mu) : mu_(mu) {
    this->mu_->Lock();
  }
  ~MutexLock() UNLOCK_FUNCTION() { this->mu_->Unlock(); }

  MutexLock(const MutexLock&) = delete;
  MutexLock& operator=(const MutexLock&) = delete;

 private:
  port::Mutex* const mu_;
};

使用时自动加锁解锁:

void LRUCache::Release(Cache::Handle* handle) {
  MutexLock l(&mutex_);
  Unref(reinterpret_cast<LRUHandle*>(handle));
}

在内存管理方面,Arena 是 LevelDB 中最重要的内存管理组件,实现了高效的批量分配:

class Arena {
 public:
  Arena();
  ~Arena();

  Arena(const Arena&) = delete;
  Arena& operator=(const Arena&) = delete;

  char* Allocate(size_t bytes);          // 非对齐分配
  char* AllocateAligned(size_t bytes);   // 对齐分配 (通常 8 或 16 字节)

  size_t MemoryUsage() const {
    return memory_usage_.load(std::memory_order_relaxed);
  }

 private:
  char* AllocateFallback(size_t bytes);
  char* AllocateNewBlock(size_t block_bytes);

  char* alloc_ptr_;                      // 当前块的分配指针
  size_t alloc_bytes_remaining_;         // 当前块剩余空间
  std::vector<char*> blocks_;            // 所有已分配的内存块
  std::atomic<size_t> memory_usage_;     // 总内存占用(原子变量,但 Arena 通常单线程使用)
};

Arena 的快速路径分配使用了 inline 优化:

inline char* Arena::Allocate(size_t bytes) {
  // 0 字节分配语义不明,直接禁止。
  // 内部代码保证了不会传 0 进来。
  assert(bytes > 0);
  if (bytes <= alloc_bytes_remaining_) {
    char* result = alloc_ptr_;
    alloc_ptr_ += bytes;
    alloc_bytes_remaining_ -= bytes;
    return result;
  }
  return AllocateFallback(bytes);
}

Arena 实现了智能的分配策略:

char* Arena::AllocateFallback(size_t bytes) {
  if (bytes > kBlockSize / 4) {
    // 超过 1/4 块大小的对象单独分配
    // 避免在标准块里留下无法利用的大碎片
    char* result = AllocateNewBlock(bytes);
    return result;
  }

  // 当前块剩下的那点空间不够了,直接废弃,开新块
  // 典型的空间换时间策略
  alloc_ptr_ = AllocateNewBlock(kBlockSize);
  alloc_bytes_remaining_ = kBlockSize;

  char* result = alloc_ptr_;
  alloc_ptr_ += bytes;
  alloc_bytes_remaining_ -= bytes;
  return result;
}

大于 1/4 块大小的请求单独分配,避免浪费。

Placement New 技术在 Arena 上构造对象时被广泛使用:

template <typename Key, class Comparator>
typename SkipList<Key, Comparator>::Node* SkipList<Key, Comparator>::NewNode(
    const Key& key, int height) {
  char* const node_memory = arena_->AllocateAligned(
      sizeof(Node) + sizeof(std::atomic<Node*>) * (height - 1));
  return new (node_memory) Node(key);
}

LRU Cache 中实现了精细的引用计数管理:

void LRUCache::Ref(LRUHandle* e) {
  if (e->refs == 1 && e->in_cache) {  // 之前只是在 LRU 热度不够,现在又有人用了,复活!移回 in_use_
    LRU_Remove(e);
    LRU_Append(&in_use_, e);
  }
  e->refs++;
}

void LRUCache::Unref(LRUHandle* e) {
  assert(e->refs > 0);
  e->refs--;
  if (e->refs == 0) {  // 即使在 Cache 里也被踢了,引用归零 -> 真正释放内存
    assert(!e->in_cache);
    (*e->deleter)(e->key(), e->value);
    free(e);
  } else if (e->in_cache && e->refs == 1) {
    // 还有 Cache 引用(refs==1),但外部不用了 -> 放入 LRU 列表等待淘汰
    LRU_Remove(e);
    LRU_Append(&lru_, e);
  }
}

资源限制器 (Limiter) 实现了信号量机制,用于限制最大并发资源数:

class Limiter {
 public:
  Limiter(int max_acquires) : acquires_allowed_(max_acquires) {}

  // 尝试获取资源,原子递减;失败则回退递增
  bool Acquire() {
    int old = acquires_allowed_.fetch_sub(1, std::memory_order_relaxed);
    if (old > 0) return true;
    acquires_allowed_.fetch_add(1, std::memory_order_relaxed);
    return false;
  }

  void Release() {
    acquires_allowed_.fetch_add(1, std::memory_order_relaxed);
  }

 private:
  std::atomic<int> acquires_allowed_;
};

除了上述核心技术,LevelDB 还展示了许多其他 C++ 技巧。Slice 是一个零拷贝的字符串视图实现:

class LEVELDB_EXPORT Slice {
 public:
  // 各种轻量构造:从 const char*、std::string 等,均为指针赋值
  Slice(const Slice&) = default;
  Slice& operator=(const Slice&) = default;

  const char* data() const { return data_; }
  size_t size() const { return size_; }

  // 不拥有所有权,clear 只是重置指针
  void clear() { data_ = ""; size_ = 0; }

  // 高效的前缀移除,只调整指针和长度
  void remove_prefix(size_t n) { data_ += n; size_ -= n; }

  // 显式深拷贝
  std::string ToString() const { return std::string(data_, size_); }

  bool starts_with(const Slice& x) const {
    return ((size_ >= x.size_) && (memcmp(data_, x.data_, x.size_) == 0));
  }

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

Slice 的核心思想是不拥有数据,只是视图,类似 C++17 的 std::string_view

LevelDB 整个项目不使用 C++ 异常,而是通过 Status 类在返回值中携带错误信息。这是 Google C++ 代码的惯例——异常会引入隐式控制流,增加推理难度,且与 RAII 之外的资源管理模式配合不佳。Status 让错误处理变得显式:调用方必须检查返回值,错误不会被悄悄吞掉。

 private:
  enum Code { kOk = 0, kNotFound = 1, kCorruption = 2,
              kNotSupported = 3, kInvalidArgument = 4, kIOError = 5 };

  // OK -> state_ 为 nullptr,零开销
  // Error -> state_ 指向堆上数组 [4字节长度, 1字节Code, Message...]
  const char* state_;

内存布局上,OK 状态不分配任何内存(state_ == nullptr),而错误信息紧凑地打包在一个 char[] 中,避免了额外的 std::string 开销。

LevelDB 实现了跨平台的字节序编码 (Endian-neutral):

inline void EncodeFixed32(char* dst, uint32_t value) {
  uint8_t* const buffer = reinterpret_cast<uint8_t*>(dst);

  // 现代编译器能识别这种模式,直接优化成 mov/str 指令(处理字节序)
  buffer[0] = static_cast<uint8_t>(value);
  buffer[1] = static_cast<uint8_t>(value >> 8);
  buffer[2] = static_cast<uint8_t>(value >> 16);
  buffer[3] = static_cast<uint8_t>(value >> 24);
}

这是小端序编码,现代编译器会优化为单条指令。

线程安全标注帮助静态分析工具检测竞态条件:

  // 互斥锁保护以下成员变量
  port::Mutex mutex_;
  std::atomic<bool> shutting_down_;
  port::CondVar background_work_finished_signal_ GUARDED_BY(mutex_);
  MemTable* mem_;
  MemTable* imm_ GUARDED_BY(mutex_);  // 正在 compact 的 immutable memtable
  std::atomic<bool> has_imm_;         // 允许后台线程在无锁情况下检查是否有 imm_

使用 GUARDED_BYEXCLUSIVE_LOCKS_REQUIRED 等标注帮助静态分析工具检测竞态条件。

变长数组技巧通过柔性数组成员实现:

struct LRUHandle {
  void* value;
  void (*deleter)(const Slice&, void* value);
  LRUHandle* next_hash;
  LRUHandle* next;
  LRUHandle* prev;
  size_t charge;
  size_t key_length;
  bool in_cache;
  uint32_t refs;      // 引用计数
  uint32_t hash;      // 预存 Hash,避免重复计算
  char key_data[1];   // 柔性数组起始位置

  Slice key() const { return Slice(key_data, key_length); }
};

通过 char key_data[1] 实现柔性数组成员:

  LRUHandle* e =
      reinterpret_cast<LRUHandle*>(malloc(sizeof(LRUHandle) - 1 + key.size()));

原子操作与内存序的精细控制在 SkipList 中得到了充分展示:

  Node* Next(int n) {
    // Acquire Load: 确保读到的节点内容已完整初始化
    return next_[n].load(std::memory_order_acquire);
  }
  void SetNext(int n, Node* x) {
    // Release Store: 确保节点初始化对其他线程可见
    next_[n].store(x, std::memory_order_release);
  }

  // 少数安全场景下可用 relaxed 版本,省去屏障开销
  Node* NoBarrier_Next(int n) { return next_[n].load(std::memory_order_relaxed); }
  void NoBarrier_SetNext(int n, Node* x) { next_[n].store(x, std::memory_order_relaxed); }

SkipList 展示了精细的内存序控制,在性能和正确性之间取得平衡。

总的来说,LevelDB 的代码展示了现代 C++ 的最佳实践。使用 = delete 禁止复制来管理资源所有权,通过 RAII 实现自动资源管理,采用抽象接口加工厂模式实现解耦与可扩展性,使用 Arena 内存池进行高效内存分配,通过 Slice 视图实现零拷贝字符串传递,用 Status 实现无异常错误处理,使用 NoDestructor 实现安全单例,以及通过线程安全标注支持静态分析。这些技术共同构成了一个高性能、可维护的 C++ 代码库。

换工作了
Linux系统性能排查