LevelDB中内存池的分析

LevelDB的内存池比较简单,只allocate,不free,这和Nginx的内存实现原理是很类似,暂时还不太明白为什么这么设计。对于Nginx而言,这样的设计有这充分的理由,那就是一次http连接的时间有限,为每次http连接创建一个新的内存池,结束之后直接销毁。一个内存池的是生命周期和一个http connection相同,将一个基于堆的对象的生命期交由业务的特性决定。

LevelDB的内存池分为一个一个的block,因为不需要进行查找,所以使用链表将这些block串起来就ok(实现其实用了vector,但是这个vector只用到了push_back的操作)。LevelDB对于block的分配和清理使用了new delete语法,而非malloc free。Arena永远只跟踪当前分配的block,如果当前需要分配的字节数大于当前block的剩余大小,则直接新分配一个block,并在新block中进行分配。但是这会出现一个问题,如果每次分配都恰好大于当前block中剩余的部分,那么会造成大量的浪费。为了解决这个问题,LevelDB规定,如果size大于1/4个blocksize,直接分配,并接入链表中。这样,内存池的使用率(不考虑free)至少是75%。

1
2
// Array of new[] allocated memory blocks
std::vector<char*> blocks_;

LevelDB提供了对于需要对齐的内存块的分配方法。这里使用了一个&取mod的小技巧,不赘言。

inline 函数的使用

所谓inline a function就是将一个函数在调用处展开,这可以省略函数调用的开销(参数压栈,出栈,汇编call调用),对于频繁调用的短函数而言,可以提高代码的运行效率。这和C++的宏定义(define)的原理其实很类似,有两点区别:

  • define不能操作类变量,所以如果需要将一个类方法内联化,只能使用inline而非define;
  • define函数不方便调试,inline函数方便调试;

inline函数虽好,但是却要理智使用。这因为,

  • 很多编译器的优化可以将一些短函数内联化,所以不需要显示的写出inline;
  • inline只节省了函数调用的开销,相比较函数的执行而言,增益不大;
  • inline函数必须写在头文件,这也一定程度上打破了模块化的设计。

对于类的成员函数而言,inline函数为什么一定要在头文件中?原理很简单,一方面,因为编译时需要展开inline函数,所以依赖于该模块的部分也都需要知悉inline的具体实现,所以如果需要将一个函数内联化,必须将其放置在头文件中;另一方面,对于在header中定义的函数,如果不标记其为内联函数,在Link期会有命名冲突的问题,导致编译失败,所以如果希望函数在头文件中定义,需将其标记为内联函数。

禁用copy拷贝构造

在Effective C++中,作者提到了如何禁用拷贝构造,在LevelDB中也能看到这样的实现细节,而且都注释了出来,代码读起来非常爽。

1
2
3
4
private:
// No copying allowed
Arena(const Arena&);
void operator=(const Arena&);

显示使用interpret_cast进行类型转换

内存池中多次使用reinterpret_cast进行显示从指针向uintptr_t的转换,是很好的编程习惯。

线程安全与原子项

在LevelDB中,存储LevelDB使用了多少空间的变量usage,使用atomic指针+interpret_cast实现。然而在对这个原子项进行操作的时候,arena一直使用了不加内存屏障(memory barrier)的方式,那么这个atomic变量其实失去了原子型,这里可以用一个unsigned int代替,整个LevelDB的内存池没有任何为多线程的特殊设计,所以arena线程不安全。