内存分配对于C程序来说是一个核心问题,许多开源软件都会针对自己软件的需要定制自己的内存分配策略,redis也不例外。然而总的来说,redis并不是专门去管内存分配的东西,它的内存分配策略的最大特点在于加上了统计信息,这一点很重要。毕竟,redis是一个内存数据库,知道自己用了多少内存,还有多少内存可用是它非常需要关注的问题。我们来看zmalloc里面的内容。
首先在zmalloc.h里面
#if defined(USE_TCMALLOC)
#define ZMALLOC_LIB ("tcmalloc-" __xstr(TC_VERSION_MAJOR) "." __xstr(TC_VERSION_MINOR))
#include <google/tcmalloc.h>
#if (TC_VERSION_MAJOR == 1 && TC_VERSION_MINOR >= 6) || (TC_VERSION_MAJOR > 1)
#define HAVE_MALLOC_SIZE 1
#define zmalloc_size(p) tc_malloc_size(p)
#else
#error "Newer version of tcmalloc required"
#endif
#elif defined(USE_JEMALLOC)
#define ZMALLOC_LIB ("jemalloc-" __xstr(JEMALLOC_VERSION_MAJOR) "." __xstr(JEMALLOC_VERSION_MINOR) "." __xstr(JEMALLOC_VERSION_BUGFIX))
#include <jemalloc/jemalloc.h>
#if (JEMALLOC_VERSION_MAJOR == 2 && JEMALLOC_VERSION_MINOR >= 1) || (JEMALLOC_VERSION_MAJOR > 2)
#define HAVE_MALLOC_SIZE 1
#define zmalloc_size(p) je_malloc_usable_size(p)
#else
#error "Newer version of jemalloc required"
#endif
#elif defined(__APPLE__)
#include <malloc/malloc.h>
#define HAVE_MALLOC_SIZE 1
#define zmalloc_size(p) malloc_size(p)
#endif
#ifndef ZMALLOC_LIB
#define ZMALLOC_LIB "libc"
#endif
这里可以看到,对于redis的内存分配,有几种可选策略,google的tmalloc,facebook的jemalloc,这两者都是malloc中的优秀实现,各有千秋。关于更多的两个malloc的讨论可以参看博文http://blog.sina.com.cn/s/blog_51df3eae01016peu.html。此外,也可以选择libc原生的malloc,但需要在每次内存分配前加4个字节记录内存分配的长度信息,以实现malloc_size的功能。源码中已经自带了jemalloc的源码,可以看出官方还是比较推崇使用jemalloc的。
下面看看最重要的部分是即内存的使用统计是如何实现的:
#if defined(__ATOMIC_RELAXED)
#define update_zmalloc_stat_add(__n) __atomic_add_fetch(&used_memory, (__n), __ATOMIC_RELAXED)
#define update_zmalloc_stat_sub(__n) __atomic_sub_fetch(&used_memory, (__n), __ATOMIC_RELAXED)
#elif defined(HAVE_ATOMIC)
#define update_zmalloc_stat_add(__n) __sync_add_and_fetch(&used_memory, (__n))
#define update_zmalloc_stat_sub(__n) __sync_sub_and_fetch(&used_memory, (__n))
#else
#define update_zmalloc_stat_add(__n) do { \
pthread_mutex_lock(&used_memory_mutex); \
used_memory += (__n); \
pthread_mutex_unlock(&used_memory_mutex); \
} while(0)
#define update_zmalloc_stat_sub(__n) do { \
pthread_mutex_lock(&used_memory_mutex); \
used_memory -= (__n); \
pthread_mutex_unlock(&used_memory_mutex); \
} while(0)
#endif
#define update_zmalloc_stat_alloc(__n) do { \
size_t _n = (__n); \
if (_n&(sizeof(long)-1)) _n += sizeof(long)-(_n&(sizeof(long)-1)); \
if (zmalloc_thread_safe) { \
update_zmalloc_stat_add(_n); \
} else { \
used_memory += _n; \
} \
} while(0)
#define update_zmalloc_stat_free(__n) do { \
size_t _n = (__n); \
if (_n&(sizeof(long)-1)) _n += sizeof(long)-(_n&(sizeof(long)-1)); \
if (zmalloc_thread_safe) { \
update_zmalloc_stat_sub(_n); \
} else { \
used_memory -= _n; \
} \
} while(0)
我们使用的大部分系统中都已经提供了关于线程安全的加减法法函数,如__atomic_add_fetch、__atomic_sub_fetch或者__sync_add_and_fetch,__sync_sub_and_fetch,如果没实现也不要紧,zmalloc里面提供了如上替代的方案,并使用宏函数进行统一起来。替代方案中我们看到了比较有意思的现象,你分配了n大小的内存,但是在统计的时候不是加上n,而是if (_n&(sizeof(long)-1)) _n += sizeof(long)-(_n&(sizeof(long)-1))这样一种方式。这是为什么呢,经过查阅资料,这是内存对齐的缘故,所谓内存对齐,就是系统在实际分配内存的时候出于效率上的考虑,会多分配一些内存给指针。对于32位机来说,4字节对齐能够使cpu访问速度提高,比如说一个long类型的变量,如果跨越了4字节边界存储,那么cpu要读取两次,这样效率就低了。但是在32位机中使用1字节或者2字节对齐,反而会使变量访问速度降低。所以这要考虑处理器类型,另外还得考虑编译器的类型。在vc中默认是4字节对齐的,GNU gcc 也是默认4字节对齐。所以在默认环境下,就以4字节对齐来计算其实际分配大小。
设计上采取了一个静态变量来表示是否启用线程安全,一个代表使用的内存数量,一个为线程锁用来进行在统计时加锁。
static size_t used_memory = 0;
static int zmalloc_thread_safe = 0;
pthread_mutex_t used_memory_mutex = PTHREAD_MUTEX_INITIALIZER;