烦请哪位老大可以介绍一下C语言中的内存对齐的原理和实现?

来源:互联网 发布:靠谱的淘宝二手显卡店 编辑:程序博客网 时间:2024/05/01 16:36

Ager发表于 2012-08-28 12:54:24

本帖最后由 Ager 于 2012-08-29 03:31 编辑

Ager 发表于 2012-08-27 23:47 static/image/common/back.gif


lenky0401         -2         不知道你在讲啥。



斑竹大虾,您也忒严厉了吧,就算是我灌水也不至于倒扣2分吧……我在CU上本来就是一个千年穷光蛋,再这麽扣下去,我要成负翁了……呵呵……:)

既然您说“不知道我在讲啥”,那么,我就试着扯扯能让大家感到reasonable的东东吧……(声明一下,我下面纯属“扯”,尽管回帖批判,不要再扣血了……)

“内存对齐”其实算个俗称,比较严格地说法(尤其是在高级编程语言的视阈来看),应该叫“数据结构对齐”。本来嘛,内存都是死的,它自己又不会听口令排队。而“对齐”其实是我们施加在内存上的一套逻辑,一种策略。

之所以有这种策略,除了斑竹您说的,是缘自它是为了满足“性能要求”的必要条件之外,还有一个原因,就是这种策略,也是满足“程序在不同架构之间的‘可移植性’的要求”(某些硬件设施只能在存储器的特定位置处access特定类型的数据或结构)的必要条件之一。

下面综合上述两点,继续扯一下。

首先说一下Cache(特指所谓“CPU高速缓存”)。基本上,Cache在高级语言程序员的视角来看,是透明的。它是完全依赖硬件设施来实现的机制,高级语言一般无法直接干预它,也无法直觉察觉它是如何运行的(不过,可以利用它)。在x86家族中,最早实现Cache机制的,大约是在1985年诞生的80386,但在此前的x86家族成员中,在数据处理的策略上,对齐早就是被固有地运用的了。所以,对齐这个策略,跟Cache并没有直接的渊源关系。不过,在后来的运用中,高级语言编程下的数据结构对齐策略之矢,确是以Cache为的而放的。

再说一下“Cache Line”,它也叫“Cache Block”,即“缓存块”。在32位运算设施上,这个“块”包含了若干个具有连续地址的存储单元,通常,单元长度就是该设施的一个Word长度(或者说“按Word长度对齐” —— 可见,“对齐”的观念早于Cache的观念),即32 bits。不过在x86-32(Intel直接称呼它为IA-32)上,这里的概念有一些混乱。IA-32为了保持对之前的16-bits设施的兼容,“Word”还是16 bits的。x86-64亦然。那么,x86-32上的 Cache Line到底是多长?不管了,对于我们高级语言的程序员来说,关心的就是如何利用Cache。那么 cat /proc/cpuinfo |grep cache 一下就知道了。【—— 关于x86这方面的事情,补充说明与一些概念上的澄清,统统给一个汇总在下面的第16楼里面。】

那么,忽略了架构的区别和Cache不说,就单说“对齐”这件事情。

上面说了,“内存对齐”不妨称作“数据结构对齐”,那么这种策略,实际上是透过把握存储单元在存储器中的位置,即“地址”来实现的,所以,也可以被称作“地址对齐”。而地址这个东西,一些高级语言是可以把握的。下面来扯扯:

CPU在access存储器的时候,有一个“粒度”概念,即CPU在一个操作单位中,只能access一个单位长度的数据,不可再分割,所谓“粒子性”。Address       Memory           Register
0              XH       ->        XH
1              XL       ->        XL
2              YH       ->        YH
3              YL       ->        YL
4              ZH
5              ZL
6              OH
7              OL上图的四个箭头,代表了CPU一次性地在内存中抓取了4个Bytes的数据到寄存器,即这个CPU的“内存access粒度”是4 Bytes的。

如果我们让CPU去抓取X这个数据(由XH+XL两个Bytes组成),那么我们就必须让access这一类数据(比如后来的Y、Z、O)的指针始终保持“从地址0开始的2 Bytes对齐”,即指针的取值只能是0、2、4、6。

否则,如果让指针不这样对齐,比如取值1开始access的话,那么,就会造成这种状况:Address       Memory           Register
0              XH                 
1              XL       ->        XL
2              YH       ->        YH
3              YL       ->        YL
4              ZH       ->        ZH
5              ZL
6              OH
7              OL这样一来,数据X肯定是抓不完整了,寄存器里原来有2个有效数据,现在只剩了1个有效数据(即数据Y),还有2个无效的数据(X的XL部分和Z的ZH部分)。如此,至少寄存器的利用率就大大折损了。

早期的68000处理器的“内存access粒度”乃是2 Bytes的,如上图所示。该处理器没有有效的处理非对齐地址的机制,而那时候的Mac OS对处理器因非对齐地址而抛出的异常也没有良好的处理机制。所以,当遭遇了上图中的非对齐地址的情况时,用户就不得不去重新启动电脑了。

最后,回到高级语言编程的环境中来,再扯扯。

当我们在C中构造一个如下的结构的时候:struct baz {
    char c;  // 假设为1个Byte
    int i;   // 假设为4个Bytes
    short s; // 假设为2个Bytes
};那么,baz类型的一个实例,将在内存中如下放置:Address       Memory         
0x0000          c               
0x0001         
0x0002         
0x0003         
0x0004          i
0x0005          i
0x0006          i
0x0007          i
0x0008          s
0x0009          s
0x000a
0x000b
0x000c         
…… ……这样,如果CPU的“内存access粒度”合适的话,那么,CPU抓取每一个成员数据的时候,都可以一次性完成(原理同前所述)。

不过,我们也可以看出,存储器中的一些位置(以Bytes为单位)上没有存放什么“有用的数据”,比如在从0x0001到0x0003这3个Bytes上。所以,在某些“特别珍惜”数据所占空间的场合里面,这个实例的数据就有可能被“打包”,比如在某些网络传输过程中,该实例就有可能这样在数据空间序列中存在:Address       Memory         
0x0000          c               
0x0001          i
0x0002          i
0x0003          i
0x0004          i
0x0005          s
0x0006          s
0x0007          牛头
0x0008          牛头
0x0009          猪面
…… ……这时候,数据存在的方式,不再是像之前那样的对齐的,而CPU去access数据的方式有可能还是按存储机构的地址对齐的。在这种情况下,如果我的意图是获取从0x0001开始的32 bits的数据,即实例的成员i的数据的话,那么,CPU有可能还是从0x0000抓取。如果CPU的“粒度”是4 Bytes,那么,它一次只能抓取到成员i的高3 Bytes的数据,(即从0x0001到0x0003上的,而0x0000上的数据没用),然后再来一次,抓取从0x0004到0x0007上的数据,其中只有0x0004上的数据有用,而从0x0005到0x0007上的成员s和半个牛头都没用。

所以,你可能就会想:有对齐,可真好呀——!

以上,仅供参考,呵呵:)


superwujc发表于 2012-08-28 22:29:34

回复 9# Ager


再请教一下,怎样在系统中查看CPU的内存访问粒度?

Ager发表于 2012-08-29 01:12:12

本帖最后由 Ager 于 2012-08-29 04:54 编辑

superwujc 发表于 2012-08-28 22:29 static/image/common/back.gif
回复 9# Ager

再请教一下,怎样在系统中查看CPU的内存访问粒度?


呵呵,不敢当啦,我也就是稀里糊涂“扯”一通而已。

关于你的这个问题,其实对于x86家族有些扯不太清楚。以前的x86即从80386直到x86_64之前的CPU,乃地道的32bits的处理器,即从内部寄存器(或字长)角度看,它一次性access数据的宽度是32bits的,但是,又因为架构从80586开始支持的external databus的宽度是64bits,所以,它一次性access内存的宽度就翻番到了64bits —— 这种机制乃是依赖于80586处理器拥有两条内部流水线同时工作,即Superscalar机制(该机制的主要前提是RISC范式)。既然Intel公司都已经把CPU发展到了这一步,那么咱们用计算机的人,也不能当这档子事不存在,所以,统统认为是64bits的就没错。

至于如何在计算机上查看CPU的内存access粒度,有好几种方法 —— 不过,不是国内中文网站上普遍宣称的那一系列“getconf xxxBIT”、“uname -xxx”之类的方法,那些方法都是错的,有的是针对OS的,不是针对CPU的。

以下方法,仅供参考:

(1)从版主提到的Cache Line角度,可以用形如以下的命令cat /sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size查看到Cache Line的长度,单位是Bytes。

如果我们知道Cache Line前缀Tag的宽度(哪位大虾请给个解法?),那么可以用公式:粒度宽度 = Tag的宽度+ lg(Cache Block的长度) + lg(Cache总数据量长度/Cache Block的长度) (注:式中“宽度者”单位均为bits;“长度者”单位均为Bytes;lg的底为2;且本公式也属于我的扯,希望有大虾验证或判否,呵呵……)

(2)直接查看CPU的状况,可以用grep flags /proc/cpuinfo在flags里,有许多项目可以作为最实在的参考标准,比如有“lm”(Long Mode)就说明必定是64bits的CPU。

如果你想进一步了解x86家族CPU的内存access的粒度(或原子性),那么,你得找一位老乞丐……

老乞丐一看到你为这事儿而来,说:“看你年纪轻轻,就对CPU和内存的事情这麽感兴趣,真是前途无量呀!……”

于是,他从破布口袋里掏出一本白皮书,书名曰《Intel® 64 and IA-32 Architectures  Software Developer’s Manual  Volume 3 (3A, 3B & 3C): System Programming Guide》…… 哇,好长!书也狂厚,大约有2000页……

“看你这麽迫不及待的样子,那就直接看书中的8.1.1吧,在第325页……”

如果老乞丐问你要钱,那你别睬他,因为这本白皮书,在网上可以免费下载阅览:wget http://download.intel.com/products/processor/manual/325384.pdf 以上,经供参考,呵呵 —— :)


superwujc发表于 2012-08-29 01:54:56

回复 16# Ager

敢问阁下是否就是传说中的老乞丐式的世外高人啊,我等菜鸟级简直要膜拜了。。。受教!!!


   

Ager发表于 2012-08-29 02:18:05

本帖最后由 Ager 于 2012-08-29 04:47 编辑

superwujc 发表于 2012-08-29 01:54 static/image/common/back.gif
回复 16# Ager

敢问阁下是否就是传说中的老乞丐式的世外高人啊,我等菜鸟级简直要膜拜了。。。受教!! ...

{:3_185:} {:3_194:} {:3_185:} 不是不是……请千万别……

我就一“扯”者,不是什么高人……

“老乞丐”一说,完全是为了插科打诨,搞笑用的。

真正的高人,在楼下登场{:3_189:}






gvim发表于 2012-08-29 02:50:03

Ager 发表于 2012-08-28 12:54 static/image/common/back.gif
斑竹大虾,您也忒严厉了吧,就算是我灌水也不至于倒扣2分吧……我在CU上本来就是一个千年穷光蛋,再这麽 ...

确实有点混乱。
有几个问题探讨吧:
>> 不过,在后来的运用中,高级语言编程下的数据结构对齐策略之矢,确是以Cache为的而放的。
内存对齐是内存对齐,cache对齐(姑且叫对齐)是cache对齐,虽然都是对齐某些地址。我不觉得内存对齐到了高级语言就是为了给cache对齐,也就是那什么之矢什么之的的说法。不管是高级语言还是低级语言,内存对齐都是体系结构的要求。换个角度来说,内存对不齐可能会crash,而cache没对齐最多就是性能稍微受影响。而且一般cache可以被控制比如设置write-though或者write-back,可以刷新cache,还可以选择替换策略等操作,而内存对齐目前没看过那个体系可以选择可对奇可不对齐或者选择按1字节还是2字节对齐的。
特定到楼主posix_memalign函数就是上面版主说的大多使用这个是为了性能的cache对齐来手工控制。
高级程序员也没见谁把内存对齐说为“数据结构对齐”。内存对齐就是内存对齐,内存是物理概念,实际看得见摸得着,数据结构是抽象概念,图上看得见,实际摸不着。牵强一些未经大众认可的说法,个人认为欠妥。

>>还有一个原因,就是这种策略,也是满足“程序在不同架构之间的‘可移植性’的要求”(某些硬件设施只能在存储器的特定位置处access特定类型的数据或结构)的必要条件之一。
内存对齐我认为也不是为了所谓的“可移植性”要求,内存对齐就是它的体系要求的,而这个体系直观而简单的例子就是x86代表的CISC和mips代表的RISC。这个在每个处理器的ABI手册上可以查,这一层都不会考虑其它体系的东西,当然专门为了其它体系而做的兼容又当另讲。ABI层没有任何不同体系间的可移植性,所谓的可移植性是上层软件赋予的比如C编译器或者Java平台。如果体系设计的时候可以稍微考虑下移植其它体系的话,那么big 和 little ending早就该消失一种了。所以个人认为这个说法也有点牵强。

>> “Cache Line”, “Cache Block”
cache line我认为也和内存对齐没什么关系,cache line确实如上面版主说的常见是64字节,根据设计可以做到128字节甚至更大,总之一般以字节为单位,而不是word或者其它什么东西。cache block不明白你指的什么,字面上看cache block算是组或路的概念吧,一般会有几条cache line,免得被太快替换。

>> 这样一来,数据X肯定是抓不完整了,寄存器里原来有2个有效数据,现在只剩了1个有效数据(即数据Y),还有2个无效的数据(X的XL部分和Z的ZH部分)
在不对齐的地址上取大于1byte的数据,处理器要么支持不对齐的访问,要么访问错误。也就是说单纯的处理器访问,要么按照你需要的byte,word,dword给你正确的东西,要么exception,不会一半对一半错。不支持非对齐访问的机器也不会自己主动发两次甚至多次访问操作来帮你取数据,如果它自己知道取两次就不会发生访问错误了。现代的话更多的是编译器越来越能搞,这样x86这样的东西编译器可以直接生成访问代码,而比如RISC这样的精简指令集,编译器需要比前者更强大的生成能力来避免非对齐的访问错误:处理器要么对齐访问要么单字节访问,再由编译器调度移位等操作生成完整的2bytes或4bytes。当然这个过程总线不上锁的话可以被打断,也就没有原子性保证了。编译器不帮你搞,还是访问错误。VLIW更依赖编译能力。

所以我个人猜测问题1,2,3某些地方你想说的是register,问题4你想说是编译器吧。

写这么多,也忒认真回复了吧,怎么的也得加8分吧,哈哈。

liuiang发表于 2012-08-29 11:09:44

本帖最后由 liuiang 于 2012-08-29 11:19 编辑

向楼上学习,顺便也唠叨几句。

针对楼主问题,我不确定是函数原型看不懂还是函数实现代码看不懂,所以胡乱说说。

内存分配会返回一个指针,那么对齐就意味着,返回的指针所指向的空间的访问地址是对齐的,

如果是2字节对齐,那么地址最低1bit一定是0,返回地址一定能被2整除,如果是4字节对齐则最低2 bits一定是0,

返回地址一定能被4整除,如果是16字节对齐则最后4bits一定是0,同样该地址可以被16整除。

原理4楼说的很清楚了,就是预先多分配一些空间,然后将返回的地址根据输入的对齐参数,向后偏移,返回即可。

实现则比较多,有一些也比较复杂,网上搜索了一下,建议你先看malloc的实现:

顺便给个链接参考:http://blog.csdn.net/dog250/article/details/5302958

malloc实现理解之后,对齐malloc其实就是一个简单变形,参考下图:  p = memalign(16, 100);
       ______
      |      |
      |      |
        ...
      |      |
      |      |
      |      |
  p-> |______| 0x-------0
      |      | 0x-------F
      |      | 0x-------E
      |      | <- 我是一个管理分配出来的内存块的数据结构对象,我不知道我的起始地址应该是多少,
      |______|    但我知道我的结束地址是根据用户参数内存对齐的,比如这个例子俺们是16字节对齐
现在楼主主要问题回答基本完成,既然大家都在扯,我也扯扯。

问题是:我们为什么需要对齐的内存?

简单的回答:效率。但根本原因是----硬件。反过来说,如果一个处理器或者一个硬件设备,他“可以”访问所有的

地址空间而不管地址是否是对齐的,并且效率完全都是一样的,那么我们可以认为,在这样的系统中,是不需要

专门进行对齐操作。但事实上这种假设在现代计算机系统中基本上是不成立的,主要包括:

1,硬件设备在进行DMA或者其他访问操作的时候,需要cache对齐或者page对齐,典型的是应用程序希望对IO设备
   进行直接操作的时候,会采用O_DIRECT选项,事实上是希望系统进行零拷贝,那么对于读写的buf就需要进行
   对齐操作,以方便硬件的访问。大部分的外设对内存对齐都会有不同程度的要求,嵌入式领域的SOC要求
   格外多。

2,Cache的对齐,楼上大侠们见解都很独到,这里不再罗嗦

3,Cpu本身访问数据的对齐要求。这里包含两个方面:

     第一是有些低端处理器,为了降低复杂度和功耗,设计的时候就需要程序指令对内存的访问必须遵循某些
         对齐要求,比如一些ARM或者MIPS。很多人觉得支持不对齐访问是个很简单的事情,其实不然,如果不对齐
         访问跨cache line,那么处理器需要同时处理器两个cache line的状态,同样推而广之,如果访存指令
         针对的地址跨了page,那么TLB要同时处理器两个page,相关问题可以对照楼上一些讨论编译器,数据类型,
         相关的论述查资料理解,这里也不展开了。

     第二是即使一些处理器支持不对齐访问,但仍旧有一些特殊指令对对齐有限制,典型的是原子指令,如x86下的
         CMPXCHG8B/16B,另外多媒体指令为了提升性能,也有一些有限制,这些都与cache和总线相关,
         这里也不展开了。

可能还有一些遗漏,希望有人可以补充。
0 0