c/c++

内存对齐相关问题的简要总结

2018-08-23  本文已影响6人  丹丘生___

查询内网中关于内存对齐的资料发现,它们往往只谈论一个层面的问题,而不涉及或稍微涉及更高或更低层面的问题;而这对于喜欢抠根问底的同学来说,是比较难受的。这里对内存对齐相关的问题和答案做一个简要总结,较为复杂的解释这里不涉及,但我会给出相关文章链接。


一、问题简述

内存对齐问题总的来说,分为How 、Who和 Why,至于What这里不再赘述:


二、问题详述

1、谁让数据在内存中对齐存放的?

答案是:编译器或某类支持该操作的语言的程序员。在C/C++中,是可以精确控制数据在内存中的分布的,目的是使CPU能够更加高效的从内存中存取数据,但其实这往往不需要开发者自己来完成,因为默认的分布已经是被编译器优化过的,实际上执行了一个填充操作,具体解释见如下链接或者后文。
VS编译器举例:Alignment


2、内存如何对齐?

内存中存储的无非是指令和数据,那么,分析数据结构如何使用内存,可以有效帮助我们认识内存对齐的具体表现形式。网上有一大堆分析C/C++ struct存储结构的文章,主要涉及了这四个关键概念和一个隐含操作:

A memory address a, is said to be n-byte aligned when a is a multiple of n bytes (where n is a power of 2). ——Data structure alignment

即一个地址是n字节的倍数,可称为n-字节对齐,而n = 2k(k=0,1....m).
所以一个地址a,如果a%(2k)=0,那么a就是(2k)-字节对齐。

举例:

#pragma pack (8)

struct S1{
    char a;
    int b;
};

struct S2{
    char c;
    struct S1 d;
    long long e;
    char f;
};

int main()
{
    struct S1 a;
    struct S2 b;

    printf("size of int, long long: %lu, %lu\n", sizeof(long),sizeof(long long));

    printf("size of S1: %lu\n", sizeof(a));
    printf("size of S2: %lu\n", sizeof(b));

    return 0;
}

输出:
size of int, long long: 4, 8
size of S1: 8
size of S2: 32

分析:

**首先分析struct S1**:
自然对齐值为4,指定对齐值为8,得到结构体有效对齐值也为4.
char a——> 0x0000 % 1 = 0,自然对齐,占一个字节
int b——> 如取值0x0001,0x0002...有0x0001 % 4 != 0,0x0002 % 4 != 0......
直至取址0x0004。
因此0x0001~0x0003将被填充(这是中间填充)。
int b 占4个字节,因此最后一个字节地址为0x0007.
结构体成员存储完毕,但我们要保证整个结构体存储完毕后,
其下一个字节地址对于该结构体是按照有效对齐值对齐的,
因为内存中有可能是连续存储着一个结构体数组。
而它的下一个字节地址为0x0008,结构体有效对齐值为4,有0x0008 % 4 = 0,
满足对齐要求,因此不必进行尾部填充。结构体大小为8字节

adr offset   element  
------   -------  
0x0000   char a;         
0x0001   char pad0[3];  //填充3字节数据
0x0004   int b;  //int b(0x0004-0x0007)
...
0x0007   int b;
------------------------------分割线-----------------------------------------------
**分析struct S2**
自然对齐值为8,指定对齐值为8,得有效对齐值为8

0x0000   char c; //1字节         
0x0001   char _pad0[7];  填充7字节数据(中间填充);
0x008     S1 d;  //占8字节 
0x0010   long long e;  //占8字节
0x0018   char f;//1字节
0x0019   char _pad[7] //尾部填充7个字节

最后一个成员char f 的地址为0x0018,下一个地址为0x0019,
0x0019 % 8 != 0,因此需要尾部填充,填充7个字节,
因此该结构体在内存中最后的位置为0x001F,因此该结构体大小为 1+7(填充)+8+8+1+7(填充)=32字节。


3、为什么要内存对齐?

前文中讲过,Why的问题要分两个层面来问,首先是为什么编译器按照内存对齐的方式存储数据?其次是,处理器为什么按照内存对齐的方式读写内存中的数据?
实际上,之所以有第一个问题,是因为第二个问题的存在,也就是说,之所以按照内存对齐方式存储数据,是因为处理器是这么做的,而且只有这么做效率才会高。

数据的内存对齐存储

对于用内存对齐的方式存储数据,其详细解释见:
Data alignment: Straighten up and fly right
翻译后的版本:link

这篇文章总结的很好,不再多复述。只分析总结其中讲述的一个细节:


Double-byte memory access granularity Quad-byte memory access granularity

上图中,分别是双字节存取粒度和四字节存取粒度的处理器。而假设数据是非内存对齐方式存储的,位于[1,2,3,4]字节处。

双字节存取粒度
当从内存中一次读取4个字节时,如果是从地址0处开始读,总共需要读2次,即第一次读[0,1],第二次读[2,3]。如果从地址1处开始读,则需要读3次,依次是[0,1],[2,3],[4,5],也就是说处理器一定是按照内存对齐的方式读取内存的,哪怕是想从地址1处开始取数据。

四字节存取粒度
从地址0开始读,只需读一次[0,1,2,3];从地址1开始读,需要读两次[0,1,2,3] 和 [4,5,6,7]。

那么,是怎么取得最终的数据的呢?

How processors handle unaligned memory access

上图很形象的描述了是如何取得最终的数据的。这里假设是MSB(大端字节序)。因为数据被存储在单元[1,2,3,4],因此按照上文所述,四字节处理器分别读取了[0,1,2,3]和[4,5,6,7],当就是把第一个值[0,1,2,3]读入到结果寄存器后,向左移动一个字节(去掉了0字节处对应的二进制数据),然后把第二个值[4,5,6,7]读入到临时寄存器,向右移动3个字节(去掉了5,6,7字节处对应的二进制数据),最后两者OR,最终结果存储于结果寄存器。

内存存取粒度:因为每次内存存取都会产生一个固定的开销,最小化内存存取次数将提升程序的性能。所以往往不是初学者认为的单字节,跟具体处理器有关,但不会出现3字节、5字节等奇数存取粒度的出现。

总的来说,内存对齐方式存储数据的目的有两点:


处理器的内存对齐存取

该问题涉及处理器的架构设计、缓存的利用等知识,具体内容待之后添加。

上一篇下一篇

猜你喜欢

热点阅读