文章开始前,先转一下大神的微博
ok,开始吧。
最近在看一些代码的时候,发现一个奇怪的设计,故写下了这篇文章。
下面举了3个例子,然后总结一下网友在水木C++版块上面的讨论。
【Case1】:
一般来说,我们会觉得,string可以这么实现:
string {
size_t size;
char* buf;
};
但是,在basic_string里,我却发现string的metadata都放一块连续的内存里面。
代码简化一下是这样子的:
string {
public:
/* when construction,buf = (new char[n]) +4; */
//【注意这里】,获取size的时候,用的数组索引时-1,
size_t size() { return (reinterpret_cast<size_t*> buf[-1]); }
char* data() { return buf; }
private:
char* buf;
};
为什么呢?
【Case2】:
在leveldb的代码实现里面,也可以看到类似的设计,把字符串类型的数据,和结构体放在一块连续的内存里面,着重看 char key_data[1] 这个成员变量。
struct LRUHandle {
void* value;
void (*deleter)(const Slice&, void* value);
LRUHandle* next_hash;
LRUHandle* next;
LRUHandle* prev;
size_t charge;
size_t key_length;
uint32_t refs;
uint32_t hash;
char key_data[1];
//【奇怪的地方】
//key_data[1]被安排到最后一个元素,当new LRUHandle的时候,是用了这样的代码:
//LRUHandle* e = reinterpret_cast<LRUHandle*>(malloc(sizeof(LRUHandle)-1 + key.size()));
//也就是把key的长度key.size()也malloc出来了,然后memcpy(e->key_data, key.data(), key.size()); 就这样用了。【第一次见过】
//我猜是为了让这些经常访问的字节紧凑地放在一块,提高性能
};
另外,女朋友给我发了在OpenBsc的源代码里面,也有类似的设计:
struct msgb {
//......
unsigned char _data[0];
};
【Test】于是我猜测,是不是和性能有关系呢? 如果把length和buf放在同一块连续的内存,读写是不是会高效一些?于是有了下面的测试:
#include <string.h>
#include <iostream>
#include <sys/time.h>
using namespace std;
class Timer {
private:
unsigned long st;
struct timeval tv;
public:
Timer():st(0) {}
unsigned long Start() {
gettimeofday(&tv, NULL);
return st = ((unsigned long)tv.tv_sec) * 1000000 + tv.tv_usec;
}
unsigned long Get() {
gettimeofday(&tv, NULL);
return ((unsigned long)tv.tv_sec) * 1000000 + tv.tv_usec - st;
}
};
#define N 10000000
void test1() {
int a = 0;
char* p = NULL;
p = new char[20];
for (int i = 0; i < N; ++i) {
++a; p[0] = 'a'; ++a; p[0] = 'b';
++a; p[0] = 'a'; ++a; p[0] = 'b';
++a; p[0] = 'a'; ++a; p[0] = 'b';
++a; p[0] = 'a'; ++a; p[0] = 'b';
++a; p[0] = 'a'; ++a; p[0] = 'b';
++a; p[0] = 'a'; ++a; p[0] = 'b';
++a; p[0] = 'a'; ++a; p[0] = 'b';
++a; p[0] = 'a'; ++a; p[0] = 'b';
++a; p[0] = 'a'; ++a; p[0] = 'b';
++a; p[0] = 'a'; ++a; p[0] = 'b';
}
}
void test2() {
char* p = NULL;
p = new char[24];
p = p+4;
int* n = &((reinterpret_cast<int*> (p))[-1]);
for (int i = 0; i < N; ++i) {
++*n; p[0] = 'a'; ++*n; p[0] = 'b';
++*n; p[0] = 'a'; ++*n; p[0] = 'b';
++*n; p[0] = 'a'; ++*n; p[0] = 'b';
++*n; p[0] = 'a'; ++*n; p[0] = 'b';
++*n; p[0] = 'a'; ++*n; p[0] = 'b';
++*n; p[0] = 'a'; ++*n; p[0] = 'b';
++*n; p[0] = 'a'; ++*n; p[0] = 'b';
++*n; p[0] = 'a'; ++*n; p[0] = 'b';
++*n; p[0] = 'a'; ++*n; p[0] = 'b';
++*n; p[0] = 'a'; ++*n; p[0] = 'b';
}
}
int main () {
Timer timer;
timer.Start();
test1();
cout << timer.Get() << endl;
timer.Start();
test2();
cout << timer.Get() << endl;
}
在我这里的测试的结果是,test2的性能更高一些,但是差距不大。
【Discussion】
1、GNU编译器gcc实现的string,是带有Copy-On-Write的,把length、ref等metadata都放在一块内存上面,可以方便的实现Copy-On-Write;2、可以节省一个指针(4byte或者8byte)
3、把buf[0]放到最后一个元素,网络协议程序经常这么干,便于memcpy、send,和从文件描述符读写
4、性能有一点点差距