[音乐] 本周的主要内容是介绍高级语言程序的机器级表示
前几讲介绍了过程调用,选择结构,和循环结构对应的
机器级代码表示,还介绍了数组,指针,结构体和联合体等
类型数据的分配,访问,及其处理逻辑对应的机器级表示
在各种类型数据的存储分配中涉及到其存放地址的对齐问题。
本讲将介绍数据的对齐方式。
所谓数据的对齐就是指
高级语言程序当中的变量啊或者常量啊,要存放在内存当中。
这个存放的这个地址,应该是相应的边界地址,按一定的边界对齐。
是因为我们的机器字长目前都是32位,64位的。
也就是说我们主存进行传送的时候, 通常是按机器字长,
或者机器字长的倍数来进行访问的。
而这个主存空间的编址方式呢又是按字节来编址的。
意思就是说同一次访问 的时候,可能会访问若干个字节,
因为我们访问的比如说是一个int 型的数据,访问了就是4个字节,站了4个地址。
如果这个传送单位是32位,就是我们从主存去访问的时候,
取来的这个数据,或者写进去的数据,传送的这个单位是32位,4个字节。
通常,按照主存的这个结构,
它是0到3字节,就第0字节到第3字节,这4个单元同时读写。
然后呢是第4,第5,第6,第7,这4个单元同时读写。
这是不可能跨的读写,比如说,如果我们要访问的这个数据
位于第3和第4字节的话,我们只能分两次。
第3字节读一次,第4字节读一次,第3单元第4单元不可能同时读。
可能同时读的只有第0到第3同时读,第4到第7同时读, 以此类推。
因此我们要按边界 对齐的话就要使得我们这个数据存放的时候
它的这个边界位于4的倍数边界上面,这样的话
我们就可以在一个存储周期就可以读一个数据了。
否则,跨边界的话我们就要多次读写数据。
刚才讲的意思就是说这个是一个主存的一个单元的示意图。
比如说这是第0单元,这是第1单元,第2单元,第3单元,每个单元 是放1个字节,因为我们是按字节编址的。
就是 每一个单元里边的编址的单位是8位。
1个字节给它一个编号,这叫按字节编址。
那么由于主存的组织结构,这个特点。
通常我们只能0,1,2,3,这4个单元一起读或者一起写。
如果是64位为单位读写的话那就是0,1 ,2 , 3,4,5,6,7,这8个单元一起读,一起写。
如果读写单位是32位呢,那就是这4个单元一起读写。
这4个单元一起读写。
主存的结构是这个样子的。
而我们指令系统当中, 支持对字节,半字,字,双字,等等的运算。
也就是说 我们指令系统比如说这个movl指令,
我们要去取第一个参数,我们前面一直 讲过,ebp加8,是在栈里面某个过程当中的第一个参数。
它如果是一个32位的,这边是表示32位的。
这么一个参数,我要把它读到寄存器里面的话,
那么这个地址,它如果不是4的倍数,比如说这个地址
是二的倍数,那么这样的话这个数据就放在 这样4个单元里面,这时候就要读2次。
所以我们指令当中给出来的这个操作数的宽度, 可以是比如这边 l 改成 b 那就是字节。
l 这个地方改成 w ,那就是16位的字。
如果 l 那就是32位,或者
q ,那就是64位,也就是指令支持的可以同时读写很多
字节,比如说8个字节,4个字节,2个字节等等。
但是我们编号的时候就是按一个字节一个字节编号的。
这样的话就是一个单位,就是我们访问的一个单位 可能会跨若干个地址,如果首地址
不是4的倍数或者8倍数的话,访问的这一个完整的一个数据就可能要 多次到主存去取或者存储。
这是一个问题所以我们在考虑
某一个数据给它分配空间的时候,这个事情当然是 编译器做的,这个编译器为每一个变量
也就是这数据分配空间的时候,它可以采用按边界对齐的方式。
比如说int 型的这些数据,那就落在
4字节的边界上面,也就是说它的这个地址呢是4的倍数,比如说这个
是一个4个字节的一个数据,它的地址一定要4的倍数,这样的话 这4个字节才可以同时读写。
如果是一个short型的,这边是 w,
那么这时候呢可以是2的倍数,只要放在这两个单元就可以一起读。
如果放在3,不是2的倍数,它就要读2次。
而字节就可以任意放,因为它本身是一个独立的一个字节,随便放哪里,只要读一次就可以了。
编译器还可以,另外一种做法,就是不管什么数据就
随便放对齐,这样的话会增加访存次数。
有关这个为什么每次读写是这样 的一种方式,我们学完存储器组织以后会更加明白。
这个刚才我们讲过,假定现在一个 字是32位,每次只能读这个32位当中
32位当然就是4个单元当中的一个两个三个四个 连续的地址。
这是前提条件。
在这个前提下面我们来考虑这样 一串变量,它在内存分配的时候,
如果按边界对齐它应该怎么分配,如果边界不对齐它应该怎么分配。
在边界对齐的情况下,这个 i 当然存放的地址应该是4的倍数。
这个k 呢应该是2的倍数,double 应该是8的倍数,所以这个x,这个变量就 不应该从这个地方开始放。
这是第4单元,第5单元,这第6单元,6不是8的倍数。
所以这两个单元得空起来,然后从这个位置开始放,这个位置的地址是8。
那么就是8的倍数,放8个字节,然后后面是 c,就是一个字节,随便放。
然后 j 呢,因为 j 是short 型的,所以它的地址应该
是2的倍数,应该16 17 18,从18开始放。
这样一来的话, i 的地址就是0,k 的地址就是 4。
x 的地址是8,c 的地址是 16, j 的地址是 18。
这样的话 x 只要两个主存周期就可以 取到了。
j 呢只要一个周期就能取到了。
如果是边界不对齐的情况,它就是挨个的放。
i 占4个字节,然后 k 占两个字节,x
接着放8个字节,c 在一个字节,j 在两个字节,这样的话
i 的地址就是0,k 的地址就是4,x 的地址就是6。
c 的地址是14,j 的地址是15。
所以这样的话我们可以看到 x 就得访问三次。
访存三次。
然后要访问 j 的时候它跨了两个
存储单位,所以这个字节要访问一次,这个字节访问一次。
所以我们可以看出边界不对齐的情况下是省了空间
省了这块空间,但是呢它会增加这个访存次数。
我们知道存储空间已经不是什么问题了,所以浪费一点存储空间没有关系。
速度是更重要的。
大家编程序的时候没有去设定它的对齐方式的时候默认的都是按边界 对齐的方式。
最简单的策略就是按数据长度对齐。
这个Windows采用的就是这种策略。
比如说 int型的数据,它是4个字节,所以它的地址呢是
4的倍数,然后short型是16位,两个字节所以它的地址是2的倍数。
double long 都是8的倍数,float 是4的倍数,char 不对齐。
而Linux呢采用的这个对齐方式更加 的宽松,short型那它就是16位。
所以它的地址是2的倍数。
其它的这些 都是四的倍数。
是Linux采用的这种对齐方式。
比如说这边有一个结构类型的数据,通常一个
结构类型的这个变量,它的首地址是按4字节边界对齐的。
默认的是按4字节边界对齐。
那么这时候我们看到 i 相对于这个结构的首地址来说它的位量当然是0。
因为本身它是按4字节边界对齐的,所以这个 i ,这个参数,
也是按4字节边界对齐,然后 si
紧接着 后面两个单元, c 是后面一个单元。
然后呢 d 这个地方的时候不能紧接着放,所以要空一个单元然后从位移量
为8的这个地方放8个字节,所以这样的话整个占了这么多个字节,就是16个字节。
因为这个结构类型, 它本身是按4字节对齐的,所以后面呢这些
分量的对齐都能满足要求。
那如果 这个结构改成这种方式, i ,si, d, c.
本身应该i 是4个字节,si 紧跟着是2个字节。
下面呢是跟的是 double 型的,所以6 7 是空的。
然后从位移量为8那个地方放8个字节,最后呢是c,这样子。
如果这个不是数组,就是一个 结构,那么它一共占17个字节, 0 到16。
但是如果我们把它说明成是一个数组, 也就是这个结构类型,后面还要跟上一个结构类型。
相同的是一个数组的话,那么要保证 下一个数组元素,它的起始位置
还是在4字节边界上面,地址还是要是4的倍数的话,
那么这边3个单元,17 18 19 还是被浪费掉了。
这样的话就能保证每一个数组元素它的起始地址都是 4的倍数。
这是对于结构数组类型的这个变量来说的。
最末的时候需要插空,也就是插一些空格,这样保证
每一个数组元素都能够按4字节边界对齐。
这两个例子,一个是 i c j,两个int型,中间插个 char。
这个是两个int型,最后是个char,如果 S1 和
S2 它都不是一个数组就是一个单独结构的话,
那么这两种结构很显然第2种的 S2的要好。
因为 S2 i j c 它不需要插空它就能满足
边界对齐的要求,而对于第一种的话,中间必须插3个字节的空,
就是浪费三个单元,然后才能保证 i c 和 j
每一个都在边界 对齐,按照边界要求对齐。
所以当然是这个结构好,因为一个是省了三个字节的空间。
但是如果这个后面是一个数组,比如 S2这个地方是个数组的话,
那么只分配9个字节那就不行了,后面的元素它不能按边界对齐。
所以还是不行,后面还是必须插3个空字节。
[音乐] [音乐]