美文网首页c/c++
内存对齐相关问题的简要总结

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

作者: 丹丘生___ | 来源:发表于2018-08-23 19:26 被阅读6次

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


    一、问题简述

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

    • Who:谁让数据在内存中对齐存放的?
    • How:内存是如何对齐的,即内存对齐的表现形式?
    • Why:为什么要内存对齐?该问题又可分成两个层面的问题:
      • 为什么内存中的数据要以内存对齐的方式排布?
      • 为什么处理器要以内存对齐的方式读取内存?

    二、问题详述

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

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


    2、内存如何对齐?

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

    • alignment :

    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)-字节对齐。

    • natural alignment :可翻译为自然对齐。如果数据的地址与其大小对齐,则称为自然对齐,否则称为未对齐。根据上文“对齐”的概念,对某变量value,如value.addr % value.size = 0,那么就可以说该变量自然对齐。

      • 基本数据类型,自然对齐值为该类型的size,如char的自然对齐值为1,int自然对齐值为4......不难理解的是,将这些变量起始位置放置于对齐边界上后(即value.addr % value.size=0处),编译器不用再对它进行任何额外的优化,猜测这应该是“自然对齐”名称的来源。
      • 对于结构体,取值为结构体内成员的natural alignment,如果结构体里不断嵌套包含结构体,那么递归的计算natural alignment,直到递归到基本数据类型,在反过来得到最外层结构体的自然对齐值。
    • specified alignment :由编译器或用户指定的对齐值(如 #progma pack (x)),只对结构体有作用。

    • effective alignment :natural alignmentspecified alignment 两者中的最小值。

    • 隐含操作:具体来说,就要执行padding(填充)操作,所谓填充,就是在结构体成员中间或最后一个成员之后填充数据占位,填充的是什么可忽略。其目的是为了满足自然对齐的要求——不仅要满足结构体成员的自然对齐要求(中间填充),还要满足结构体本身自然对齐的要求(尾部填充)。

    举例:

    #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字节等奇数存取粒度的出现。

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

    • 提高存取效率
    • 因为有的处理器不支持非内存对齐方式存取,将影响可移植性。

    处理器的内存对齐存取

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

    相关文章

      网友评论

        本文标题:内存对齐相关问题的简要总结

        本文链接:https://www.haomeiwen.com/subject/gmvpiftx.html