前言
相信大家对内存对齐这个概念一定都比较熟悉,本文将介绍,如何利用内存对齐这一特性来做一些有意思的探索。
至于为什么要使用内存对齐,这是一个比较复杂的问题,简单来说就是提高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