先介绍三个概念:
1.自身对齐值:数据类型本身的对齐值,例如char为1 short为2 int、float为4 double为8
2.指定对齐值:编译器默认对齐值或者使用#pragma pack(value)指定的值
3.有效对齐值:取自身对齐值和指定对齐值中较小的那个
对齐规则:
1.不仅结构体的成员变量有有效对齐值,结构体本身也有对齐值,这主要是考虑到结构体的数组,对于结构体或者类,要将其补齐为其有效对齐值的整数倍。结构体的有效对齐值是起最大数据成员的自身对齐值(若有指定对齐值 则取两者小的)。
2.存放结构成员的地址必须是该成员的有效对齐值的整数倍
下面来验证一下:
typedef struct __attribute__ ((__packed__)){
char a;
double ab;
char b;
}no_pack;
typedef struct {
char a;
double ab;
char b;
}default_pack;
#pragma pack(4)
typedef struct{
char a;
double ab;
char b;
}given_pack;
#define show_info(typename, var) printf("%s size = %d, \n a in %p\nab in %p\n b in %p\n", #typename, sizeof(var), &var.a, &var.ab, &var.b)
int main(int argc, char const *argv[])
{
no_pack no;
default_pack df;
given_pack gp;
show_info(no_pack, no);
show_info(default_pack, df);
show_info(given_pack, gp);
return 0;
}
no_pack结构体是在定义时告诉编译器该结构不需要字节对齐,所以会看到a占一个字节ab占用8个字节b占用1个字节 no_pack的大小为实际占用的内存大小10
default_pack结构体是使用默认的规则进行字节对齐a占用了一个字节之后ab应该要占用8个字节 根据对齐规则2 必须得偏移7个字节之后的地址才是ab对齐值的整数倍 b占用1个字节此时占用内存大小为17字节,但是根绝规则1,结构体需要填充7个字节才能满足其有效对齐值的整数倍的要求,所以default_pack的大小为24
given_pack结构体使用了指定对齐值4,因此成员变量的地址要是4的倍数a占用一个字节之后 根据对齐规则2必须得偏移3个字节之后才能满足ab变量放置条件,接着b占用1个字节此时占用内存大小为13字节,根绝规则1,需要填充3个字节,大小为16
思考一下这么做会带来什么问题?
聪明的你一定想到,字节对齐会造成浪费,同样的数据成员 default_pack比no_pack每个变量要多占用14byte的空间,而且是浪费掉,特别在系统长时间运行,业务量很大的时候,会造成很大的内存浪费耶。所以为什么需要字节对齐?
字节对齐的意义
"各个硬件平台对存储空间的处理上有很大的不同,一些平台对某些特定类型的数据只能从某些特定地址开始存取。比如有些架构的CPU在访问 一个没有进行对齐的变量的时候会发生错误,那么在这种架构下编程必须保证字节对齐.其他平台可能没有这种情况,但是最常见的是如果不按照适合其平台要求对 数据存放进行对齐,会在存取效率上带来损失。比如有些平台每次读都是从偶地址开始,如果一个int型(假设为32位系统)如果存放在偶地址开始的地方,那么一个读周期就可以读出这32bit,而如果存放在奇地址开始的地方,就需要2个读周期,并对两次读出的结果的高低字节进行拼凑才能得到该32bit数据。"
由此可知,因为硬件平台的差异,字节对齐在某些硬件平台上可以让cpu更加高效。所以字节对齐是一种牺牲内存换取cpu效率的无奈之举。
那么我们应该怎么做?
1. 在定义结构体或者联合的时候,尽量把自身对齐值较大的先定义,比如default_pack先定义成员变量ab再分别定义a、b此时结构体的大小为16,又省了8byte...
2.尝试使用pack自定义对齐值大小 一般定义为结构体使用较多的成员类型的大小
=======================================================================================================================================
可变长结构体
what,可变长结构体是啥?结构体还需要可变长吗?成员变量定义一个指针,通过malloc想要多长就多长呀 还讨论可变长结构体有什么必要?
答案是当然有必要,对于极简主义来说,指针也占用内存啊
可变长结构的优点:
1. 不需要初始化,数组名就是偏移的地址
2.不占用任何空间
可变长结构的缺点:
当数据区大小发生改变时,需要重新分配一块更大的内存整块移动(realloc)
定义的方式:
//普通定义
struct stringbuffer{
unsigned int len;
unsigned int valid;
char *buf;
};
//可变长结构体的定义
struct stringbuffer{
unsigned int len;
unsigned int valid;
char buf[];
};
*你会发现 普通定义的结构体比可变长结构体大小多了一个指针的大小
由此延伸到redis的sds.h的源码分析
redis底层定义了五个数据结构最基础的结构用于存储,每个结构体都包含一个buf
初始化时:
sds sdsnewlen(const void *init, size_t initlen) {
void *sh;
sds s;
char type = sdsReqType(initlen);
//默认使用sds_type_8
if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
int hdrlen = sdsHdrSize(type);
unsigned char *fp; /* flags pointer. */
sh = s_malloc(hdrlen+initlen+1);//指向结构体地址
if (init==SDS_NOINIT)
init = NULL;
else if (!init)
memset(sh, 0, hdrlen+initlen+1);
if (sh == NULL) return NULL;
s = (char*)sh+hdrlen;//指向结构体的buf指针
fp = ((unsigned char*)s)-1;//指向结构体flag的地址
//根据不同类型的结构体初始化成员
switch(type) {
case SDS_TYPE_5: {
*fp = type | (initlen << SDS_TYPE_BITS);
break;
}
case SDS_TYPE_8: {
SDS_HDR_VAR(8,s);
sh->len = initlen;
sh->alloc = initlen;
*fp = type;
break;
}
case SDS_TYPE_16: {
SDS_HDR_VAR(16,s);
sh->len = initlen;
sh->alloc = initlen;
*fp = type;
break;
}
case SDS_TYPE_32: {
SDS_HDR_VAR(32,s);
sh->len = initlen;
sh->alloc = initlen;
*fp = type;
break;
}
case SDS_TYPE_64: {
SDS_HDR_VAR(64,s);
sh->len = initlen;
sh->alloc = initlen;
*fp = type;
break;
}
}
if (initlen && init)
memcpy(s, init, initlen);
s[initlen] = '\0';
return s;
}
sds扩容时操作:
sds sdsMakeRoomFor(sds s, size_t addlen) {
void *sh, *newsh;
//剩余可用长度
size_t avail = sdsavail(s);
size_t len, newlen;
//s[-1]比较隐晦
char type, oldtype = s[-1] & SDS_TYPE_MASK;
int hdrlen;
//如果剩余可用长度还能满足 直接返回 不需要扩容
if (avail >= addlen) return s;
//s申请的长度
len = sdslen(s);
//计算指向结构体首地址
sh = (char*)s-sdsHdrSize(oldtype);
//新的长度
newlen = (len+addlen);
//如果新长度小于阀值1024*1024则成倍增长
if (newlen < SDS_M*AX_PREALLOC)
newlen *= 2;
else
newlen += SDS_MAX_PREALLOC;
//获取到新的长度的结构应该使用什么类型的结构体
type = sdsReqType(newlen);
/* Don't use type 5: the user is appending to the string and type 5 is
* not able to remember empty space, so sdsMakeRoomFor() must be called
* at every appending operation. */
if (type == SDS_TYPE_5) type = SDS_TYPE_8;
hdrlen = sdsHdrSize(type);
//如果需要的长度当前结构体仍能满足 则进行realloc操作
if (oldtype==type) {
newsh = s_realloc(sh, hdrlen+newlen+1);
if (newsh == NULL) return NULL;
s = (char*)newsh+hdrlen;
} else {
//否则迁移到满足需求的新结构体
/* Since the header size changes, need to move the string forward,
* and can't use realloc */
newsh = s_malloc(hdrlen+newlen+1);
if (newsh == NULL) return NULL;
memcpy((char*)newsh+hdrlen, s, len+1);
s_free(sh);
s = (char*)newsh+hdrlen;
s[-1] = type;
sdssetlen(s, len);
}
sdssetalloc(s, newlen);
return s;
}
sds[-1]这个操作比较隐晦
sds指向结构体的buf指针,buf是个char类型的指针,因此buf[-1]则是结构体距离buf最近的上一个char变量,也就是flags