关于c语言内存地址对齐的一点思考
关于c语言内存地址对齐的一点思考
算法与编程之美 发表于4年前
关于c语言内存地址对齐的一点思考
  • 发表于 4年前
  • 阅读 6872
  • 收藏 124
  • 点赞 0
  • 评论 40

腾讯云 新注册用户 域名抢购1元起>>>   

前言

相信大家对内存对齐这个概念一定都比较熟悉,本文将介绍,如何利用内存对齐这一特性来做一些有意思的探索。

至于为什么要使用内存对齐,这是一个比较复杂的问题,简单来说就是提高cpu access memory的性能,后续有时间就内存对齐这个问题,展开详细的探讨。

示例

首先来看一个简单的示例:

假设我们现在要用c语言做一个简单的学生信息管理系统,学生结构体有三个基本属性,分别是年龄(0-100),性别(male:0, female:1),姓名(字符串大小10以内)。在编码之前,我们需要对系统进行设计,而设计阶段最重要的莫过于数据结构。本题涉及的结构体非常简单,结构体student定义如下:

struct student {
    char age;
    char sex;
    char *name;
};

 

相信上面这个结构体是大多数人得出的结果,那么这个结构体的定义是不是最优的呢或者说是内存利用率是最高的呢?

分析

在具体的探讨之前,我们先来介绍一下关于内存对齐的一个小知识点:如果某变量内存地址4字节对齐,则该地址的低2位必为0。这个应该比较好理解,因为4字节对齐,内存地址必须为4的倍数,所以低2位必然为0,否则不能满足要求。

在了解这个知识点之后,我们再来对上面的student结构体做一点修改。

我们定义一个字符数组name用来存放学生姓名,且该结构体4字节对齐,定义如下:

 char name[10] __attribute__ ((aligned(4))) = "hellooooo";

 

从上面的知识点,我们知道字符数组name的低两位为0,换句话说,这两位是没有用到的,既然如此,我们是否可以考虑利用这两位来做一些文章呢?

我们对上面的student结构体做如下修改:

struct student{
    char age;
    unsigned long name_sex;
};

 

我们将sex和name字段合二为一,用一个字段name_sex来表示,这样做是否可行呢?

答案是可行的。

#define stu_get_name(stu) ((char *)((stu.name_sex) & ~3))
#define stu_get_sex(stu) ((stu.name_sex) & 1)

char name[10] __attribute__ ((aligned(4))) = "hellooooo";

struct student stu;
stu.age = 10;
stu.name_sex = (unsigned long)name | 1;

printf("name: %s \n", stu_get_name(stu));
printf("sex: %d \n", stu_get_sex(stu));

 

我们先定义了一个字符数组name且该数组内存地址4字节对齐,即低两位为0。接着我们将该地址的第0位置1用来保存学生性别字段,然后赋值为student结构体的name_sex字段。

那么我们如何得到student结构体的name字段的值呢?答案很简单,只需要将name_sex字段的低两位置0就可以得到我们所需要的name字段值,而name_sex的第0位即((stu.name_sex) & 1)就是我们student结构体中的sex字段,上述示例中,sex值为1,即性别为female。

至此,我们利用内存地址对齐的特性,修改了我们示例最先提出的student结构体。

本文中我们利用4字节内存对齐的低两位为0这一特性,将其最低位用来存放学生性别,从而达到高效的利用内存。

总结

本文的重点并不在于介绍如何设计一个学生信息管理系统,示例中的结构体只是为了说明内存对齐的应用,借助学生信息管理系统这样的一个场景来介绍,我们在设计结构体的时候,利用内存对齐的特性,可以更加灵活的设计我们所需要的结构体,从而达到对内存的高效利用。

注1:如对内存对齐的应用感兴趣,可进一步参考linux内核中rbtree的设计,其rb_parent_color字段就是利用了内存对齐的特性,将结点的父结点parent以及该结点的颜色color两个字段合二为一。

注2:本空间《**思考》系列博文都是基于linux内核,用平实的语言和简单的示例,描述linux内核中一些比较有意思的设计,希望能够和大家一起探索linux内核设计的奥秘。

注3:@中山野鬼 老师的两句点评非常精辟,受益匪浅,和大家一起分享下,前辈总是能够一语道破个中玄机:

楼主记得,内存对齐的处理逻辑,一定要和计算逻辑分开。有关联的地方使用宏的方式就可以。否则以后你有苦头吃。而且会额外增加计算逻辑的复杂度。
有些事情不是底层可以帮你更好的处理的。一个简单的例子,你去设计一个数据结构,比如树吧,对节点的访问逻辑,一旦你固定,则不会有改变,但是每个节点的存储空间的实际访问,则会根据存储方式的改变而改变,通常是用宏的方式,进行调整。这样的调整不会影响整体逻辑,但是会改变数据计算过程中,对数据访问的存储空间
所谓内存对其,其实和内存申请没有关系,只是和具体对象(不是面向对象的对象)的寻址有关系。比如,你要对一个对象进行数据读取或者写入,你总是先要计算地址,然后进行访问。 而计算地址是根据逻辑来的。通过计算地址进行直接存储访问,则存在一个逻辑转换,确保每个数据对齐。这里增加个宏,由此实现分离。 简单的例子,我们逻辑上连续存储24位像素,假设(通常一行内不会如此)我们希望每个像素的存储是32位对齐。那么你访问每个像素,存在(x,y,z)三个变量,x,y是一个平面的列数,和行数,Z是层级数。 假设B是基地址。则如下操作 #define image_pixel_byte_size 4 #define get_bias(x,y,z) ((z) * X * Y + (y) * Y + x) #define get_store(B,n) ((BYTE)B + n * image_pixel_byte_size) #define get_pixel(p,x,y,z) get_store(p,get_bias(x,y,z)) 上面,实际内存对齐操作,是通过 get_store 的宏实现的。其实这里还存在逻辑,但逻辑中存在一个对齐的数值定义。 不同过多介意宏里面有宏,实际编译,这些东西都会被优化掉。但对代码组织,是有很大帮助的。哈
除非是模板,否则类的化,会固化方法。这对逻辑的松耦合不能带来任何好处。设计,有时需要紧耦合,有时需要松耦合,其实判断他们该松还是紧,要根据这个设计的来源是否存在关联判定。比如,数据的逻辑提取和实际数据的存储,一个来源业务要求的算法,一个来源于业务所运行的系统,因此需要松耦合,而在一个算法中的逻辑设计,则存在紧耦合。哈。这块,比较绕口令,需要实践体会。

注4:后续还是要对本文的示例做一些修改,本文的示例的确很不恰当,不过还是能够清晰的表达我的意思;

注5:本文的评论也值得大家阅读和思考,很多知识点要想彻底的搞明白需要非常深厚的功底,面对别人的质疑你是否能够从原理上说明白,是一项挑战;

引用

【1】http://gcc.gnu.org/onlinedocs/gcc/Variable-Attributes.html

【2】http://stackoverflow.com/questions/381244/purpose-of-memory-alignment

【3】http://en.wikipedia.org/wiki/Data_structure_alignment

如果您对算法或编程感兴趣,欢迎扫描下方二维码并关注公众号“算法与编程之美”,和您一起探索算法和编程的神秘之处,给您不一样的解题分析思路。

 

共有 人打赏支持
粉丝 229
博文 68
码字总数 67291
评论 (40)
苗哥
原来四字节对齐还可以这样用啊,长见识了……
卖姑娘的小火柴
有个疑问,如果这样强制4字节对齐的话,如果name在分配内存时,特别是在栈里,是不是同样会造成空间浪费?有点得不偿失
算法与编程之美

引用来自“卖姑娘的小火柴”的评论

有个疑问,如果这样强制4字节对齐的话,如果name在分配内存时,特别是在栈里,是不是同样会造成空间浪费?有点得不偿失

本文只是为了强调内存对齐的应用,所以在代码中使用了强制的4字节对齐,事实上,
gcc编译的时候会默认采用内存对齐,内存对齐的确会造成一定的内存浪费,
但这种内存对齐方式对于cpu access memory来说,会带来非常大的性能提升,具体的可
参考http://stackoverflow.com/questions/381244/purpose-of-memory-alignment。
本文在默认的内存对齐的基础上,使用其特性来存储sex字段。
卖姑娘的小火柴

引用来自“justin_cn”的评论

引用来自“卖姑娘的小火柴”的评论

有个疑问,如果这样强制4字节对齐的话,如果name在分配内存时,特别是在栈里,是不是同样会造成空间浪费?有点得不偿失

本文只是为了强调内存对齐的应用,所以在代码中使用了强制的4字节对齐,事实上,
gcc编译的时候会默认采用内存对齐,内存对齐的确会造成一定的内存浪费,
但这种内存对齐方式对于cpu access memory来说,会带来非常大的性能提升,具体的可
参考http://stackoverflow.com/questions/381244/purpose-of-memory-alignment。
本文在默认的内存对齐的基础上,使用其特性来存储sex字段。

嗯,的确是这样的,发现gcc在传参的时候,参数小于32位的,它全当32位处理了
算法与编程之美

引用来自“卖姑娘的小火柴”的评论

引用来自“justin_cn”的评论

引用来自“卖姑娘的小火柴”的评论

有个疑问,如果这样强制4字节对齐的话,如果name在分配内存时,特别是在栈里,是不是同样会造成空间浪费?有点得不偿失

本文只是为了强调内存对齐的应用,所以在代码中使用了强制的4字节对齐,事实上,
gcc编译的时候会默认采用内存对齐,内存对齐的确会造成一定的内存浪费,
但这种内存对齐方式对于cpu access memory来说,会带来非常大的性能提升,具体的可
参考http://stackoverflow.com/questions/381244/purpose-of-memory-alignment。
本文在默认的内存对齐的基础上,使用其特性来存储sex字段。

嗯,的确是这样的,发现gcc在传参的时候,参数小于32位的,它全当32位处理了

这个是因为函数调用的时候,实参是保存在寄存器中而并非stack frame中,64位操作系统中有6个寄存器rdi,rsi,rdx,rcx,r8,r9,如果多于6个参数则保存在stack frame中,32位操作系统不太了解具体有几个。
卖姑娘的小火柴

引用来自“justin_cn”的评论

引用来自“卖姑娘的小火柴”的评论

引用来自“justin_cn”的评论

引用来自“卖姑娘的小火柴”的评论

有个疑问,如果这样强制4字节对齐的话,如果name在分配内存时,特别是在栈里,是不是同样会造成空间浪费?有点得不偿失

本文只是为了强调内存对齐的应用,所以在代码中使用了强制的4字节对齐,事实上,
gcc编译的时候会默认采用内存对齐,内存对齐的确会造成一定的内存浪费,
但这种内存对齐方式对于cpu access memory来说,会带来非常大的性能提升,具体的可
参考http://stackoverflow.com/questions/381244/purpose-of-memory-alignment。
本文在默认的内存对齐的基础上,使用其特性来存储sex字段。

嗯,的确是这样的,发现gcc在传参的时候,参数小于32位的,它全当32位处理了

这个是因为函数调用的时候,实参是保存在寄存器中而并非stack frame中,64位操作系统中有6个寄存器rdi,rsi,rdx,rcx,r8,r9,如果多于6个参数则保存在stack frame中,32位操作系统不太了解具体有几个。

不是这样吧,x86寄存器本来就少,一般不用来传参的啊,我碰到的情况都是保存在栈里
算法与编程之美

引用来自“卖姑娘的小火柴”的评论

引用来自“justin_cn”的评论

引用来自“卖姑娘的小火柴”的评论

引用来自“justin_cn”的评论

引用来自“卖姑娘的小火柴”的评论

有个疑问,如果这样强制4字节对齐的话,如果name在分配内存时,特别是在栈里,是不是同样会造成空间浪费?有点得不偿失

本文只是为了强调内存对齐的应用,所以在代码中使用了强制的4字节对齐,事实上,
gcc编译的时候会默认采用内存对齐,内存对齐的确会造成一定的内存浪费,
但这种内存对齐方式对于cpu access memory来说,会带来非常大的性能提升,具体的可
参考http://stackoverflow.com/questions/381244/purpose-of-memory-alignment。
本文在默认的内存对齐的基础上,使用其特性来存储sex字段。

嗯,的确是这样的,发现gcc在传参的时候,参数小于32位的,它全当32位处理了

这个是因为函数调用的时候,实参是保存在寄存器中而并非stack frame中,64位操作系统中有6个寄存器rdi,rsi,rdx,rcx,r8,r9,如果多于6个参数则保存在stack frame中,32位操作系统不太了解具体有几个。

不是这样吧,x86寄存器本来就少,一般不用来传参的啊,我碰到的情况都是保存在栈里

x86不太了解,x86_64是这样做的。
小苏打

引用来自“justin_cn”的评论

引用来自“卖姑娘的小火柴”的评论

引用来自“justin_cn”的评论

引用来自“卖姑娘的小火柴”的评论

引用来自“justin_cn”的评论

引用来自“卖姑娘的小火柴”的评论

有个疑问,如果这样强制4字节对齐的话,如果name在分配内存时,特别是在栈里,是不是同样会造成空间浪费?有点得不偿失

本文只是为了强调内存对齐的应用,所以在代码中使用了强制的4字节对齐,事实上,
gcc编译的时候会默认采用内存对齐,内存对齐的确会造成一定的内存浪费,
但这种内存对齐方式对于cpu access memory来说,会带来非常大的性能提升,具体的可
参考http://stackoverflow.com/questions/381244/purpose-of-memory-alignment。
本文在默认的内存对齐的基础上,使用其特性来存储sex字段。

嗯,的确是这样的,发现gcc在传参的时候,参数小于32位的,它全当32位处理了

这个是因为函数调用的时候,实参是保存在寄存器中而并非stack frame中,64位操作系统中有6个寄存器rdi,rsi,rdx,rcx,r8,r9,如果多于6个参数则保存在stack frame中,32位操作系统不太了解具体有几个。

不是这样吧,x86寄存器本来就少,一般不用来传参的啊,我碰到的情况都是保存在栈里

x86不太了解,x86_64是这样做的。

fastcall调用约定就是通过寄存器来传递参数的,只不过当参数过多时还是要通过栈的。
http://msdn.microsoft.com/en-us/library/6xa169sk.aspx
文中的存储sex字段的技巧最好别用,虽然char name[10]强制4byte对齐,所以name指针的值的低2bit总是0,所以才可以这么用。但是操作指针,特别是对指针进行位运算太过于晦涩,如果这个指针是malloc的结果,free时没有把低2bit清零,是很危险的。
kenping
做学问研究,教学的时候这样这样做,但是实际应用不建议这样,节约几个字节的内存,要多杀死看代码的人多少脑细胞?
翠屏阿姨

引用来自“kenping”的评论

做学问研究,教学的时候这样这样做,但是实际应用不建议这样,节约几个字节的内存,要多杀死看代码的人多少脑细胞?

对啊,设计数据结构也要关心未来的扩展,不增加几个保留字都说不过去
qiao_xf@163.com
不建议这样“节省”内存,真要节省那点内存,不如直接用1字节对齐得了。
KevinJen
真逗,把性别字段放在年龄里多好,操作简单,还有就不知道使用位域吗?
neo-chen
struct student stu;
stu.age = 10;
stu.name_sex = (unsigned long)name | 1;

没看明白,C里面不是用 -> 吗?
应该是 stu->age = 10;
KevinJen

引用来自“陈景峰”的评论

struct student stu;
stu.age = 10;
stu.name_sex = (unsigned long)name | 1;

没看明白,C里面不是用 -> 吗?
应该是 stu->age = 10;

指针才用->,指向运算符,java没有指针
子矜

引用来自“kenping”的评论

做学问研究,教学的时候这样这样做,但是实际应用不建议这样,节约几个字节的内存,要多杀死看代码的人多少脑细胞?

赞同~ 我大Java程序员 动不动一个小应用就开10G以上内存 毫不关心内存~
mental

引用来自“翠屏阿姨”的评论

引用来自“kenping”的评论

做学问研究,教学的时候这样这样做,但是实际应用不建议这样,节约几个字节的内存,要多杀死看代码的人多少脑细胞?

对啊,设计数据结构也要关心未来的扩展,不增加几个保留字都说不过去

ff的monkeyspider的JS引擎的变量也是这样设计的, 后面两个BIT表示数据类型,前面30BIT才表示有效数据,它的特点就是所有的对象指针 ,整数,浮点数都可以用一个4个字节的变量表示
codewu
楼主你好:
struct student {
char age;
char sex;
char *name;
};这个在x86_64上默认8字节对齐为16字节长,而
struct student{
char age;
unsigned long name_sex;
};也是16字节,你把sex和name合二为一不是增加了复杂度了吗?更不要说效率了吧,现在age独占8字节,浪费了7字节。
求解。。。
五杀联盟
碉堡了
chao_83
也不差那一位的空间, 这种用法太不直观了.
建议在问题的角度思考,而不是硬件的角度.
算法与编程之美

引用来自“chao_83”的评论

也不差那一位的空间, 这种用法太不直观了.
建议在问题的角度思考,而不是硬件的角度.

个人认为c程序员还是多应该从硬件的角度去思考问题,吝啬内存的使用。
×
算法与编程之美
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
* 金额(元)
¥1 ¥5 ¥10 ¥20 其他金额
打赏人
留言
* 支付类型
微信扫码支付
打赏金额:
已支付成功
打赏金额: