内存对齐 订阅
内存对齐”应该是编译器的“管辖范围”。编译器为程序中的每个“数据单元”安排在适当的位置上。但是C语言的一个特点就是太灵活,太强大,它允许你干预“内存对齐”。如果你想了解更加底层的秘密,“内存对齐”对你就不应该再模糊了。 展开全文
内存对齐”应该是编译器的“管辖范围”。编译器为程序中的每个“数据单元”安排在适当的位置上。但是C语言的一个特点就是太灵活,太强大,它允许你干预“内存对齐”。如果你想了解更加底层的秘密,“内存对齐”对你就不应该再模糊了。
信息
特    征
透明
作    用
每个数据单元安排在适当的位置
释    义
是编译器的“管辖范围”。
中文名
内存对齐
内存对齐词条简介
对于大部分程序员来说,“内存对齐”对他们来说都应该是“透明的”。
收起全文
精华内容
下载资源
问答
  • 内存对齐
    2020-11-24 08:46:23

    内存地址对齐,是一种在计算机内存中排列数据(表现为变量的地址)、访问数据(表现为CPU读取数据)的一种方式,包含了两种相互独立又相互关联的部分:基本数据对齐和结构体数据对齐 。

    为什么需要内存对齐?对齐有什么好处?是我们程序员来手动做内存对齐呢?还是编译器在进行自动优化的时候完成这项工作?

    在现代计算机体系中,每次读写内存中数据,都是按字(word,4个字节,对于X86架构,系统是32位,数据总线和地址总线的宽度都是32位,所以最大的寻址空间为232 = 4GB,按A[31,30…2,1,0]这样排列,但是请注意为了CPU每次读写 4个字节寻址,A[0]和A[1]两位是不参与寻址计算的。)为一个块(chunks)来操作(而对于X64则是8个字节为一个块)即每次读取4个或者8个字节的数据。注意,这里说的 CPU每次读取的规则,并不是变量在内存中地址对齐规则。既然是这样的,如果变量在内存中存储的时候也按照这样的对齐规则,就可以加快CPU读写内存的速 度,当然也就提高了整个程序的性能,并且性能提升是客观,虽然当今的CPU的处理数据速度(是指逻辑运算等,不包括取址)远比内存访问的速度快,程序的执 行速度的瓶颈往往不是CPU的处理速度不够,而是内存访问的延迟,虽然当今CPU中加入了高速缓存用来掩盖内存访问的延迟,但是如果高密集的内存访问,一 种延迟是无可避免的,内存地址对齐会给程序带来了很大的性能提升。

    内存地址对齐是计算机语言自动进行的,也即是编译器所做的工作。但这不意味着我们程序员不需要做任何事情,因为如果我们能够遵循某些规则,可以让编译器做得更好,毕竟编译器不是万能的。

    为了更好理解上面的意思,这里给出一个示例。在32位系统中,假如一个int变量在内存中的地址是0x00ff42c3,因为int是占用4个字节,所以它的尾地址应该是0x00ff42c6,这个时候CPU为了读取这个int变量的值,就需要先后读取两个word大小的块,分别是0x00ff42c00x00ff42c3和0x00ff42c40x00ff42c7,然后通过移位等一系列的操作来得到,在这个计算的过程中还有可能引起一些总线数据错误的。但是如果编译器对变量地址进行了对齐,比如放在0x00ff42c0,CPU就只需要一次就可以读取到,这样的话就加快读取效率。

    1、基本数据对齐
    在X86,32位系统下基于Microsoft、Borland和GNU的编译器,有如下数据对齐规则:
    a、一个char(占用1-byte)变量以1-byte对齐。
    b、一个short(占用2-byte)变量以2-byte对齐。
    c、一个int(占用4-byte)变量以4-byte对齐。
    d、一个long(占用4-byte)变量以4-byte对齐。
    e、一个float(占用4-byte)变量以4-byte对齐。
    f、一个double(占用8-byte)变量以8-byte对齐。
    g、一个long double(占用12-byte)变量以4-byte对齐。
    h、任何pointer(占用4-byte)变量以4-byte对齐。

            而在64位系统下,与上面规则对比有如下不同:
             a、一个long(占用8-byte)变量以8-byte对齐。
             b、一个double(占用8-byte)变量以8-byte对齐。
             c、一个long double(占用16-byte)变量以16-byte对齐。
             d、任何pointer(占用8-byte)变量以8-byte对齐。
    

    2、结构体数据对齐
    结构体数据对齐,是指结构体内的各个数据对齐。在结构体中的第一个成员的首地址等于整个结构体的变量的首地址,而后的成员的地址随着它声明的顺序和实际占用的字节数递增。为了总的结构体大小对齐,会在结构体中插入一些没有实际意思的字符来填充(padding)结构体。

    在结构体中,成员数据对齐满足以下规则:
    a、结构体中的第一个成员的首地址也即是结构体变量的首地址。
    b、结构体中的每一个成员的首地址相对于结构体的首地址的偏移量(offset)是该成员数据类型长度的整数倍。
    c、结构体的总大小是对齐模数(对齐模数等于#pragma pack(n)所指定的n与结构体中最大数据类型的成员大小的最小值)的整数倍。

    更多相关内容
  • 内存对齐,memory alignment.为了提高程序的性能,数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;然而,对齐的内存访问仅需要一次访问。内存对齐...
  • 内存对齐的初步讲解 内存对齐可以用一句话来概括: “数据项只能存储在地址是数据项大小的整数倍的内存位置上” 例如int类型占用4个字节,地址只能在0,4,8等位置上。 例1: 代码如下:#include <stdio>struct xx{ ...
  • 内存对齐,memory alignment.为了提高程序的性能,数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;然而,对齐的内存访问仅需要一次访问。  内存...
  • C++中的内存对齐实例详解 内存对齐  在我们的程序中,数据结构还有变量等等都需要占有内存,在很多系统中,它都要求内存分配的时候要对齐,这样做的好处就是可以提高访问内存的速度。 我们还是先来看一段简单的...
  • #pragma pack() :取消内存对齐访问 #pragma pack(n) (n=1/2/4/8):按n字节对齐 #pragma pack(2) struct mystruct1 { int a; char b; short c; } struct mystruct2 { int a;; double b; short c; } ...
  • 深入内存对齐的详解

    2020-09-05 09:53:51
    本篇文章是对内存对齐进行了详细的分析介绍,需要的朋友参考下
  • C++ 内存对齐原则及作用 C++ 内存对齐原则及作用 C++ 内存对齐原则及作用
  •  0 约定和预备知识 0.1 地址边界 如果把字节看作小房子,内存就是顺序排列的小房子。每个小房子都有一个顺序编号的门牌号码,例如:0,1,2,...,0xffffffff。我们 把这个门牌号码称作地址。本文将2的整数倍的地址记...
  • 内存对齐详解

    2019-01-13 17:31:45
    详细解读内存对齐原则,通过实例让你完全掌握内存对齐
  • 主要给大家总结了关于C++面试题中结构体内存对齐计算问题的相关资料,文中通过示例代码介绍的非常详细,通过这些介绍的内容对大家在面试C++工作的时候,会有一定的参考帮助,需要的朋友们下面随着小编来一起学习学习...
  • 内存对齐内存对齐规则解释、内存对齐原理

    千次阅读 多人点赞 2020-03-28 23:44:31
    一、内存对齐的原因 我们都知道计算机是以字节(Byte)为单位划分的,理论上来说CPU是可以访问任一编号的字节数据的,我们又知道CPU的寻址其实是通过地址总线来访问内存的,CPU又分为32位和64位,在32位的CPU一次...

    一、内存对齐的原因

    我们都知道计算机是以字节(Byte)为单位划分的,理论上来说CPU是可以访问任一编号的字节数据的,我们又知道CPU的寻址其实是通过地址总线来访问内存的,CPU又分为32位和64位,在32位的CPU一次可以处理4个字节(Byte)的数据,那么CPU实际寻址的步长就是4个字节,也就是只对编号是4的倍数的内存地址进行寻址。同理64位的CPU的寻址步长是8字节,只对编号是8的倍数的内存地址进行寻址,如下图所示是64位CPU的寻址示意图:
    在这里插入图片描述

    这样做可以实现最快速的方式寻址且不会遗漏一个字节,也不会重复寻址。

    那么对于程序而言,一个变量的数据存储范围是在一个寻址步长范围内的话,这样一次寻址就可以读取到变量的值,如果是超出了步长范围内的数据存储,就需要读取两次寻址再进行数据的拼接,效率明显降低了。例如一个double类型的数据在内存中占据8个字节,如果地址是8,那么好办,一次寻址就可以了,如果是20呢,那就需要进行两次寻址了。这样就产生了数据对齐的规则,也就是将数据尽量的存储在一个步长内,避免跨步长的存储,这就是内存对齐。在32位编译环境下默认4字节对齐,在64位编译环境下默认8字节对齐。

    查看自己电脑是多少位操作系统
    终端下输入:uname -a 回车
    x86_64 表示系统为64位
    i686 表示系统32位的

    二、对齐规则
    1:数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员的偏移为 #pragma pack 指定的数值和这个数据成员自身长度中较小那个的整数倍。
    2:数据成员为结构体:如果结构体的数据成员还为结构体,则该数据成员的“自身长度”为其内部最大元素的大小。(struct a 里存有 struct b,b 里有char,int,double等元素,那 b “自身长度”为 8)
    3:结构体的整体对齐规则:在数据成员按照 #1 完成各自对齐之后,结构体本身也要进行对齐。对齐会将结构体的大小增加为 #pragma pack 指定的数值和结构体最大数据成员长度中较小那个的整数倍。

    (看不懂没关系,因为我也没看懂,也是打印之后才明白)

    三、实例

    typedef struct test1 {
        char a;//1
        int b;//4字节
        double c;//8
        char d[11];//11
    }Test1;//数据成员
    
    int main(int argc, const char * argv[]) {
    
        Test1 t1;
        //xcode默认对齐系数8,即#pragma pack(8)
        //char a,     1<8按1对齐,offset=0            [0]
        //int b,      4<8按4对齐,char a占到[0],int b应该从地址1开始排, 地址1不是对齐数4的倍数,所以int b的首地址是从4的最小倍数开始,所以offset=4,       [4...7];
        //double c,   8=8按8对齐,int b已经占到[4...7],double c从地址8开始排,地址8是对齐数8的倍数,所以double c的首地址是从8的最小倍数开始,offse=8             [8...15]
        //char d[11], 1<8按1对齐,double c已经占到[8...15], char d[11]从地址16开始,地址16是对齐数1的倍数,所以char d的,offset=16,          存储位置[16...26]
        //最后为27位,又因为27不是内部最大成员中double 8位字节的倍数,所以补齐为32)
        printf(" %lu\n %p\n %p\n %p\n %p\n", sizeof(t1), &t1.a, &t1.b, &t1.c, &t1.d);
        
        return 0;
    }
    

    结果:

     32
     0x7ffeefbff568
     0x7ffeefbff56c
     0x7ffeefbff570
     0x7ffeefbff578
    

    以首地址0x7ffeefbff568为offset=0,
    0x68=104,
    0x6c=108,
    0x70=112,
    0x78=120,
    0x7ffeefbff56c相对于0x7ffeefbff568便宜了4,
    0x7ffeefbff570相对于0x7ffeefbff568便宜了8,
    0x7ffeefbff578相对于0x7ffeefbff568偏移了16,符合规律
    上面int b正好偏移4,double c正好偏移8,比较凑巧,如果char a[5]呢,使地址正好措开,此时int b应该是[8…13],double c应该是[16…23], char d[11]应该是[24…34],34不是最大double c的整数倍,补齐到最小倍数40,所以结构体的大小应该是40.

     40
     0x7ffeefbff560
     0x7ffeefbff568
     0x7ffeefbff570
     0x7ffeefbff578
    Program ended with exit code: 0
    

    在结构体struct Test1中添加一个结构体Test2,看下结果:

    typedef struct test2 {
        char a[13];//1      [0...13]
        double b;//8        [16...23]
        int c[11];//4       [24...67]
        float d;//4         [68...71]
    }Test2;
    
    typedef struct test1 {
        char a[5];//1       [0...4]
        int b;//4           [8...11]
        double c;//8        [16...23]
        char d[11];//11     [24...34]
        Test2 t2;//         [40...111] Test2的'自身长度'为double b=8,所以从8的最小倍数开始,即40
    }Test1;//数据成员
    //此时:
    //  char a[13];//1      [40...52]
    //  double b;//8        [56...63]
    //  int c[11];//4       [64...107]
    //  float d;//4         [107...111]
    
    int main(int argc, const char * argv[]) {
        
        Test1 t1;
        Test2 t2;
        printf(" %lu\n", sizeof(t2));
        printf(" %lu\n", sizeof(t1));
        return 0;
    }
    

    结果:

     72
     112
    Program ended with exit code: 0
    

    把Test2中的double b注销了,看下结果:

    typedef struct test2 {
        char a[13];//1      [0...13]
        //double b;//8        [16...23]
        int c[11];//4       [16...59]
        float d;//4         [60...63]
    }Test2;
    
    typedef struct test1 {
        char a[5];//1       [0...4]
        int b;//4           [8...11]
        double c;//8        [16...23]
        char d[11];//11     [24...34]
        Test2 t2;//64       [36...99] 此时Test2的'自身长度'为int c=4,所以从4的最小倍数开始,即36开始,又因为100不是double c的倍数,补齐到最小倍数104
    }Test1;//数据成员
    
    int main(int argc, const char * argv[]) {
        
        Test1 t1;
        Test2 t2;
        printf(" %lu\n", sizeof(t2));
        printf(" %lu\n", sizeof(t1));
        return 0;
    }
    
     64
     104
    Program ended with exit code: 0
    

    四、更改默认对齐系数

    #pragma pack(2)//1、2、4、8、16,以2为例
    
    typedef struct test2 {
        char a[13];//1      [0...13]
        //double b;//8        [16...23]
        int c[11];//4    2<4   [14...57]从2的最小倍数开始,即14
        float d;//4         [58...61]
    }Test2;
    int main(int argc, const char * argv[]) {
        
        Test2 t2;
        printf(" %lu\n", sizeof(t2));
        return 0;
    }
    

    结果:

     62
    Program ended with exit code: 0
    

    参考:

    https://www.jianshu.com/p/f01fe1ef892d

    展开全文
  • 详解内存对齐

    千次阅读 2021-08-17 01:00:22
    结构体的内存对齐规则 一提到内存对齐,大家都喜欢拿结构体的内存对齐来举例子,这里要提醒大家一下,不要混淆了一个概念,其他类型也都是要内存对齐的,只不过拿结构体来举例子能更好的理解内存对齐,并且结构体中...

    欢迎大家点击上方文字「Golang梦工厂」关注公众号,设为星标,第一时间接收推送文章。

    前言

    哈喽,大家好,我是asong。好久不见,上周停更了一周,因为工作有点忙,好在这周末闲了下来,就赶紧来肝文喽。今天我们来聊一聊一道常见的面试八股文——内存对齐,我们平常在业务开发中根本不care内存对齐,但是在面试中,这就是一个高频考点,今天我们就一起来看一看到底什么是内存对齐。

    前情概要

    在了解内存对齐之前,先来明确几个关于操作系统的概念,更加方面我们对内存对齐的理解。

    • 内存管理:我们都知道内存是计算中重要的组成之一,内存是与CPU进行沟通的桥梁,用于暂存CPU中的运算数据、以及与硬盘等外部存储器交换的数据。早期,程序是直接运行在物理内存上的,直接操作物理内存,但是会存在一些问题,比如使用效率低、地址空间不隔离等问题,所以就出现了虚拟内存,虚拟内存就是在程序和物理内存之间引入了一个中间层,这个中间层就是虚拟内存,这样就达到了对进程地址和物理地址的隔离。在linux系统中,将虚拟内存划分为用户空间内核空间,用户进程只能访问用户空间的虚拟地址,只有通过系统调用、外设中断或异常才能访问内核空间,我们主要来看一下用户空间,用户空间被分为5个不同内存区域:

      内存的知识先介绍个大概,对于本文的理解应该够了,我们接着介绍操作系统几个其他概念。

      • 代码段:存放可执行文件的操作指令,只读

      • 数据段:用来存放可执行文件中已初始化全局变量,存放静态变量和全局变量

      • BSS段:用来存未初始化的全局变量

      • 栈区:用来存临时创建的局部变量

      • 堆区:用来存动态分配的内存段

    • CPU:中央处理单元(Cntral Pocessing Unit)的缩写,也叫处理器;CPU是计算机的运算核心和控制核心,我们人类靠着大脑思考,电脑就是靠着CPU来运算、控制,起到协调和控制作用,从功能来看,CPU 的内部由寄存器、控制器、运算器和时钟四部分组成,各部分之间通过电信号连通。

    • CPU和内存的工作关系:当我们执行一个程序时,首先由输入设备向CPU发出操作指令,CPU接收到操作指令后,硬盘中对应的程序就会被直接加载到内存中,此后,CPU 再对内存进行寻址操作,将加载到内存中的指令翻译出来,而后发送操作信号给操作控制器,实现程序的运行或数据的处理。存在于内存中的目的就是为了CPU能够过总线进行寻址,取指令、译码、执行取数据,内存与寄存器交互,然后CPU运算,再输出数据至内存。

    • osos全称为Operating System,也就是操作操作系统,是一组主管并控制计算机操作、运用和运行硬件、软件资源和提供公共服务组织用户交互的相互关联的系统软件,同时也是计算机系统的内核与基石。

    • 编译器:编译器就是将“一种语言(通常为高级语言)”翻译为“另一种语言(通常为低级语言)”的程序。一个现代编译器的主要工作流程:源代码 (source code) → 预处理器(preprocessor) → 编译器 (compiler) → 目标代码 (object code) → 链接器 (Linker) → 可执行程序(executables)。

    写在最后的一个知识点:

    计算机中,最小的存储单元为字节,理论上任意地址都可以通过总线进行访问,每次寻址能传输的数据大小就跟CPU位数有关。常见的CPU位数有8位,16位,32位,64位。位数越高,单次操作执行的数据量越大,性能也就越强。os的位数一般与CPU的位数相匹配,32CPU可以寻址4GB内存空间,也可以运行32位的os,同样道理,64位的CPU可以运行32位的os,也可以运行64位的os

    何为内存对齐

    以下内容来源于网络总结:

    现代计算机中内存空间都是按照字节(byte)进行划分的,所以从理论上讲对于任何类型的变量访问都可以从任意地址开始,但是在实际情况中,在访问特定类型变量的时候经常在特定的内存地址访问,所以这就需要把各种类型数据按照一定的规则在空间上排列,而不是按照顺序一个接一个的排放,这种就称为内存对齐,内存对齐是指首地址对齐,而不是说每个变量大小对齐。

    为何要有内存对齐

    主要原因可以归结为两点:

    • 有些CPU可以访问任意地址上的任意数据,而有些CPU只能在特定地址访问数据,因此不同硬件平台具有差异性,这样的代码就不具有移植性,如果在编译时,将分配的内存进行对齐,这就具有平台可以移植性了

    • CPU每次寻址都是要消费时间的,并且CPU 访问内存时,并不是逐个字节访问,而是以字长(word size)为单位访问,所以数据结构应该尽可能地在自然边界上对齐,如果访问未对齐的内存,处理器需要做两次内存访问,而对齐的内存访问仅需要一次访问,内存对齐后可以提升性能。举个例子:

    假设当前CPU32位的,并且没有内存对齐机制,数据可以任意存放,现在有一个int32变量占4byte,存放地址在0x00000002 - 0x00000005(纯假设地址,莫当真),这种情况下,每次取4字节的CPU第一次取到[0x00000000 - 0x00000003],只得到变量1/2的数据,所以还需要取第二次,为了得到一个int32类型的变量,需要访问两次内存并做拼接处理,影响性能。如果有内存对齐了,int32类型数据就会按照对齐规则在内存中,上面这个例子就会存在地址0x00000000处开始,那么处理器在取数据时一次性就能将数据读出来了,而且不需要做额外的操作,使用空间换时间,提高了效率。

    没有内存对齐机制:

    内存对齐后:

    对齐系数

    每个特定平台上的编译器都有自己的默认"对齐系数",常用平台默认对齐系数如下:

    • 32位系统对齐系数是4

    • 64位系统对齐系数是8

    这只是默认对齐系数,实际上对齐系数我们是可以修改的,之前写C语言的朋友知道,可以通过预编译指令#pragma pack(n)来修改对齐系数,因为C语言是预处理器的,但是在Go语言中没有预处理器,只能通过tags命名约定来让Go的包可以管理不同平台的代码,但是怎么修改对齐系数,感觉Go并没有开放这个参数,找了好久没有找到,等后面再仔细看看,找到了再来更新!

    既然对齐系数无法更改,但是我们可以查看对齐系数,使用Go语言中的unsafe.Alignof可以返回相应类型的对齐系数,使用我的mac(64位)测试后发现,对齐系数都符合2^n这个规律,最大也不会超过8

    func main()  {
     fmt.Printf("string alignof is %d\n", unsafe.Alignof(string("a")))
     fmt.Printf("complex128 alignof is %d\n", unsafe.Alignof(complex128(0)))
     fmt.Printf("int alignof is %d\n", unsafe.Alignof(int(0)))
    }
    运行结果
    string alignof is 8
    complex128 alignof is 8
    int alignof is 8
    

    注意:不同硬件平台占用的大小和对齐值都可能是不一样的。

    结构体的内存对齐规则

    一提到内存对齐,大家都喜欢拿结构体的内存对齐来举例子,这里要提醒大家一下,不要混淆了一个概念,其他类型也都是要内存对齐的,只不过拿结构体来举例子能更好的理解内存对齐,并且结构体中的成员变量对齐有自己的规则,我们需要搞清这个对齐规则。

    C语言的对齐规则与Go语言一样,所以C语言的对齐规则对Go同样适用:

    • 对于结构体的各个成员,第一个成员位于偏移为0的位置,结构体第一个成员的偏移量(offset)为0,以后每个成员相对于结构体首地址的offset都是该成员大小与有效对齐值中较小那个的整数倍,如有需要编译器会在成员之间加上填充字节。

    • 除了结构成员需要对齐,结构本身也需要对齐,结构的长度必须是编译器默认的对齐长度和成员中最长类型中最小的数据大小的倍数对齐。

    举个例子

    根据上面的对齐规则,我们来分析一个例子,加深理解:

    // 64位平台,对齐参数是8
    type User struct {
     A int32 // 4
     B []int32 // 24
     C string // 16
     D bool // 1
    }
    
    func main()  {
     var u User
     fmt.Println("u1 size is ",unsafe.Sizeof(u))
    }
    // 运行结果
    u size is  56
    

    这里我的mac64位的,对齐参数是8int32[]int32stringbool对齐值分别是4881,占用内存大小分别是424161,我们先根据第一条对齐规则分析User

    • 第一个字段类型是int32,对齐值是4,大小为4,所以放在内存布局中的第一位.

    • 第二个字段类型是[]int32,对齐值是8,大小为24,按照第一条规则,偏移量应该是成员大小24与对齐值8中较小那个的整数倍,那么偏移量就是8,所以4-7位会由编译进行填充,一般为0值,也称为空洞,第932位为第二个字段B.

    • 第三个字段类型是string,对齐值是8,大小为16,所以他的内存偏移值必须是8的倍数,因为user前两个字段就已经排到了第32位,所以offset32正好是8的倍数,不要填充,从32位到48位是第三个字段C.

    • 第四个字段类型是bool,对齐值是1,大小为1,所以他的内存偏移值必须是1的倍数,因为user前两个字段就已经排到了第48位,所以下一位的偏移量正好是48,正好是字段D的对齐值的倍数,不用填充,可以直接排列到第四个字段,也就是从48到第49位是第三个字段D.

    根据第一条规则分析后,现在结构所占大小为49字节,我们再来根据第二条规则分析:

    • 根据第二条规则,默认对齐值是8,字段中最大类型程度是24,所以求出结构体的对齐值是8,我们目前的内存长度是49,不是8的倍数,所以需要补齐,所以最终的结果就是56,补了7位。

    成员变量顺序对内存对齐带来的影响

    根据上面的规则我们可以看出,成员变量的顺序也会影响内存对齐的结果,我们先来看一个例子:

    type test1 struct {
     a bool // 1
     b int32 // 4
     c string // 16
    }
    
    type test2 struct {
     a int32 // 4
     b string // 16
     c bool // 1
    }
    
    
    func main()  {
     var t1 test1
     var t2 test2
    
     fmt.Println("t1 size is ",unsafe.Sizeof(t1))
     fmt.Println("t2 size is ",unsafe.Sizeof(t2))
    }
    

    运行结果:

    t1 size is  24
    t2 size is  32
    

    test1的内存布局:

    test2的内存布局:

    )

    通过以上分析,我们可以看出,结构体中成员变量的顺序会影响结构体的内存布局,所以在日常开发中大家要注意这个问题,可以节省内存空间。

    空结构体字段对齐

    Go语言中空结构体的大小为0,如果一个结构体中包含空结构体类型的字段时,通常是不需要进行内存对齐的,举个例子:

    type demo1 struct {
     a struct{}
     b int32
    }
    
    func main()  {
     fmt.Println(unsafe.Sizeof(demo1{}))
    }
    运行结果:
    4
    

    从运行结果可知结构体demo1占用的内存与字段b占用内存大小相同,所以字段a是没有占用内存的,但是空结构体有一个特例,那就是当 struct{} 作为结构体最后一个字段时,需要内存对齐。因为如果有指针指向该字段, 返回的地址将在结构体之外,如果此指针一直存活不释放对应的内存,就会有内存泄露的问题(该内存不因结构体释放而释放),所以当struct{}作为结构体成员中最后一个字段时,要填充额外的内存保证安全。

    type demo2 struct {
     a int32
     b struct{}
    }
    
    func main()  {
     fmt.Println(unsafe.Sizeof(demo2{}))
    }
    运行结果:
    8
    

    考虑内存对齐的设计

    在之前的文章源码剖析sync.WaitGroup分析sync.waitgroup的源码时,使用state1来存储状态:

    // A WaitGroup must not be copied after first use.
    type WaitGroup struct {
     noCopy noCopy
    
     // 64-bit value: high 32 bits are counter, low 32 bits are waiter count.
     // 64-bit atomic operations require 64-bit alignment, but 32-bit
     // compilers do not ensure it. So we allocate 12 bytes and then use
     // the aligned 8 bytes in them as state, and the other 4 as storage
     // for the sema.
     state1 [3]uint32
    }
    

    state1这里总共被分配了12个字节,这里被设计了三种状态:

    • 其中对齐的8个字节作为状态,高32位为计数的数量,低32位为等待的goroutine数量

    • 其中的4个字节作为信号量存储

    提供了(wg *WaitGroup) state() (statep *uint64, semap *uint32)帮助我们从state1字段中取出他的状态和信号量,为什么要这样设计呢?

    因为64位原子操作需要64位对齐,但是32位编译器不能保证这一点,所以为了保证waitGroup32位平台上使用的话,就必须保证在任何时候,64位操作不会报错。所以也就不能分成两个字段来写,考虑到字段顺序不同、平台不同,内存对齐也就不同。因此这里采用动态识别当前我们操作的64位数到底是不是在8字节对齐的位置上面,我们来分析一下state方法:

    // state returns pointers to the state and sema fields stored within wg.state1.
    func (wg *WaitGroup) state() (statep *uint64, semap *uint32) {
     if uintptr(unsafe.Pointer(&wg.state1))%8 == 0 {
      return (*uint64)(unsafe.Pointer(&wg.state1)), &wg.state1[2]
     } else {
      return (*uint64)(unsafe.Pointer(&wg.state1[1])), &wg.state1[0]
     }
    }
    

    当数组的首地址是处于一个8字节对齐的位置上时,那么就将这个数组的前8个字节作为64位值使用表示状态,后4个字节作为32位值表示信号量(semaphore)。同理如果首地址没有处于8字节对齐的位置上时,那么就将前4个字节作为semaphore,后8个字节作为64位数值。画个图表示一下:

    )

    总结

    终于接近尾声了,内存对齐一直面试中的高频考点,通过内存对齐可以了解面试者对操作系统知识的了解程度,所以这块知识还是比较重要的,希望这篇文章能帮助大家答疑解惑,更好的忽悠面试官~。

    文中代码已上传github:https://github.com/asong2020/Golang_Dream/tree/master/code_demo/memory 欢迎star;

    文中有任何问题欢迎留言区探讨~;

    素质三连(分享、点赞、在看)都是笔者持续创作更多优质内容的动力!我是asong,我们下期见。

    创建了一个Golang学习交流群,欢迎各位大佬们踊跃入群,我们一起学习交流。入群方式:关注公众号获取。更多学习资料请到公众号领取。

    推荐往期文章:

    展开全文
  • Go语言内存对齐详解

    2022-03-22 16:14:04
    本篇文章我们介绍了内存对齐相关的知识,并介绍了unsafe包中的相关方法,最后具体示例一步步演示了内存对齐,学习内存对齐,一篇就够!

    你必须非常努力,才能看起来毫不费力!

    微信搜索公众号[ 漫漫Coding路 ],一起From Zero To Hero !

    前言

    前面有篇文章我们学习了 Go语言空结构体,最近又在看 unsafe包 的知识,在查阅相关资料时不免会看到内存对齐相关的内容,虽然感觉这类知识比较底层,但是看到了却不深究和渣男有什么区别?虽然我不会,但我可以学🐶,那么这篇文章,我们就一起来看下什么是内存对齐吧!

    说明:本文中的测试示例,均是基于Go1.17 64位机器

    基础知识

    在Go语言中,我们可以通过 unsafe.Sizeof(x) 来确定一个变量占用的内存字节数(不包含 x 所指向的内容的大小)。

    例如对于字符串数组,在64位机器上,unsafe.Sizeof() 返回的任意字符串数组大小为 24 字节,和其底层数据无关:

    func main() {
    	s := []string{"1", "2", "3"}
    	s2 := []string{"1"}
    	fmt.Println(unsafe.Sizeof(s))  // 24
    	fmt.Println(unsafe.Sizeof(s2)) // 24
    }
    

    对于Go语言的内置类型,占用内存大小如下:

    类型字节数
    bool1个字节
    intN, uintN, floatN, complexNN/8 个字节 (int32 是 4 个字节)
    int, uint, uintptr计算机字长/8 (64位 是 8 个字节)
    *T, map, func, chan计算机字长/8 (64位 是 8 个字节)
    string (data、len)2 * 计算机字长/8 (64位 是 16 个字节)
    interface (tab、data 或 _type、data)2 * 计算机字长/8 (64位 是 16 个字节)
    []T (array、len、cap)3 * 计算机字长/8 (64位 是 24 个字节)
    func main() {
    	fmt.Println(unsafe.Sizeof(int(1)))                  // 8
    	fmt.Println(unsafe.Sizeof(uintptr(1)))				      // 8
    	fmt.Println(unsafe.Sizeof(map[string]string{}))		  // 8
    	fmt.Println(unsafe.Sizeof(string("")))				      // 16
    	fmt.Println(unsafe.Sizeof([]string{}))				      // 24
    
    	var a interface{}
    	fmt.Println(unsafe.Sizeof(a))						            // 16
    }
    

    看个问题

    基于上面的理解,那么对于一个结构体来说,占用内存大小就应该等于多个基础类型占用内存大小的和,我们就结合几个示例来看下:

    type Example struct {
    	a bool // 1个字节
    	b int	 // 8个字节
    	c string // 16个字节
    }
    
    func main() {
    	fmt.Println(unsafe.Sizeof(Example{})) // 32
    }
    

    Example 结构体的三个基础类型,加起来一个 25字节,但是最终输出的却是 32字节

    我们再看两个结构体,即使这两个结构体包含的字段类型一致,但是顺序不一致,最终输出的大小也不一样

    type A struct {
    	a int32
    	b int64
    	c int32
    }
    
    type B struct {
    	a int32
    	b int32
    	c int64
    }
    
    func main() {
    	fmt.Println(unsafe.Sizeof(A{})) // 24
    	fmt.Println(unsafe.Sizeof(B{})) // 16
    }
    

    是什么导致了上述问题的呢,这就引出了我们要看的知识点:内存对齐

    什么是内存对齐

    我们知道,在计算机中访问一个变量,需要访问它的内存地址,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是:在访问特定类型变量的时候通常在特定的内存地址访问,这就需要对这些数据在内存中存放的位置有限制,各种类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,这就是对齐。

    内存对齐是编译器的管辖范围。表现为:编译器为程序中的每个“数据单元”安排在适当的位置上。

    为什么需要内存对齐

    1. 有些CPU可以访问任意地址上的任意数据,而有些CPU只能在特定地址访问数据,因此不同硬件平台具有差异性,这样的代码就不具有移植性,如果在编译时,将分配的内存进行对齐,这就具有平台可以移植性了。

    2. CPU 访问内存时并不是逐个字节访问,而是以字长(word size)为单位访问,例如 32位的CPU 字长是4字节,64位的是8字节。如果变量的地址没有对齐,可能需要多次访问才能完整读取到变量内容,而对齐后可能就只需要一次内存访问,因此内存对齐可以减少CPU访问内存的次数,加大CPU访问内存的吞吐量

    假设每次访问的步长为4个字节,如果未经过内存对齐,获取b的数据需要进行两次内存访问,最后再进行数据整理得到b的完整数据:

    image-20220313230839425

    如果经过内存对齐,一次内存访问就能得到b的完整数据,减少了一次内存访问:

    image-20220313231143302

    unsafe.AlignOf()

    unsafe.AlignOf(x) 方法的返回值是 m,当变量进行内存对齐时,需要保证分配到 x 的内存地址能够整除 m。因此可以通过这个方法,确定变量x 在内存对齐时的地址:

    • 对于任意类型的变量 x ,unsafe.Alignof(x) 至少为 1。
    • 对于 struct 结构体类型的变量 x,计算 x 每一个字段 f 的 unsafe.Alignof(x.f)unsafe.Alignof(x) 等于其中的最大值。
    • 对于 array 数组类型的变量 x,unsafe.Alignof(x) 等于构成数组的元素类型的对齐倍数。

    对于系统内置基础类型变量 x ,unsafe.Alignof(x) 的返回值就是 min(字长/8,unsafe.Sizeof(x)),即计算机字长与类型占用内存的较小值:

    func main() {
      fmt.Println(unsafe.Alignof(int(1))) // 1 -- min(8,1)
      fmt.Println(unsafe.Alignof(int32(1))) // 4 -- min (8,4)
    	fmt.Println(unsafe.Alignof(int64(1))) // 8 -- min (8,8)
      fmt.Println(unsafe.Alignof(complex128(1))) // 8 -- min(8,16)
    }  
    

    内存对齐规则

    我们讲内存对齐,就是把变量放在特定的地址,那么如何计算特定地址呢,这就涉及到内存对齐规则:

    • 成员对齐规则

    针对一个基础类型变量,如果 unsafe.AlignOf() 返回的值是 m,那么该变量的地址需要 被m整除 (如果当前地址不能整除,填充空白字节,直至可以整除)。

    • 整体对齐规则

    针对一个结构体,如果 unsafe.AlignOf() 返回值是 m,需要保证该结构体整体内存占用是 m的整数倍,如果当前不是整数倍,需要在后面填充空白字节。

    通过内存对齐后,就可以在保证在访问一个变量地址时:

    1. 如果该变量占用内存小于字长:保证一次访问就能得到数据;
    2. 如果该变量占用内存大于字长:保证第一次内存访问的首地址,是该变量的首地址。

    举个例子

    例1:

    type A struct {
    	a int32
    	b int64
    	c int32
    }
    
    func main() {
    	fmt.Println(unsafe.Sizeof(A{1, 1, 1}))  // 24
    }
    
    1. 第一个字段是 int32 类型,unsafe.Sizeof(int32(1))=4,内存占用为4个字节,同时unsafe.Alignof(int32(1)) = 4,内存对齐需保证变量首地址可以被4整除,我们假设地址从0开始,0可以被4整除:

    成员变量1内存对齐

    1. 第二个字段是 int64 类型,unsafe.Sizeof(int64(1)) = 8,内存占用为 8 个字节,同时unsafe.Alignof(int64(1)) = 8,需保证变量放置首地址可以被8整除,当前地址为4,距离4最近的且可以被8整除的地址为8,因此需要添加四个空白字节,从8开始放置:

    成员变量2内存对齐

    1. 第三个字段是 int32 类型,unsafe.Sizeof(int32(1))=4,内存占用为4个字节,同时unsafe.Alignof(int32(1)) = 4,内存对齐需保证变量首地址可以被4整除,当前地址为16,16可以被4整除:

    成员变量3内存对齐

    1. 所有成员对齐都已经完成,现在我们需要看一下整体对齐规则:unsafe.Alignof(A{}) = 8,即三个变量成员的最大值,内存对齐需要保证该结构体的内存占用是 8 的整数倍,当前内存占用是 20个字节,因此需要再补充4个字节:

    整体对齐

    1. 最终该结构体的内存占用为 24字节。

    例二:

    type B struct {
    	a int32
    	b int32
    	c int64
    }
    
    func main() {
    	fmt.Println(unsafe.Sizeof(B{1, 1, 1}))  // 16
    }
    
    1. 第一个字段是 int32 类型,unsafe.Sizeof(int32(1))=4,内存占用为4个字节,同时unsafe.Alignof(int32(1)) = 4,内存对齐需保证变量首地址可以被4整除,我们假设地址从0开始,0可以被4整除:

    成员变量1内存对齐

    1. 第二个字段是 int32 类型,unsafe.Sizeof(int32(1))=4,内存占用为4个字节,同时unsafe.Alignof(int32(1)) = 4,内存对齐需保证变量首地址可以被4整除,当前地址为4,4可以被4整除:

    成员变量2内存对齐

    1. 第三个字段是 int64 类型,unsafe.Sizeof(int64(1))=8,内存占用为8个字节,同时unsafe.Alignof(int64(1)) = 8,内存对齐需保证变量首地址可以被8整除,当前地址为8,8可以被8整除:

    成员变量3内存对齐

    1. 所有成员对齐都已经完成,现在我们需要看一下整体对齐规则:unsafe.Alignof(B{}) = 8,即三个变量成员的最大值,内存对齐需要保证该结构体的内存占用是 8 的整数倍,当前内存占用是 16个字节,已经符合规则,最终该结构体的内存占用为 16个字节。

    空结构体的对齐规则

    如果空结构体作为结构体的内置字段:当变量位于结构体的前面和中间时,不会占用内存;当该变量位于结构体的末尾位置时,需要进行内存对齐,内存占用大小和前一个变量的大小保持一致。

    type C struct {
    	a struct{}
    	b int64
    	c int64
    }
    
    type D struct {
    	a int64
    	b struct{}
    	c int64
    }
    
    type E struct {
    	a int64
    	b int64
    	c struct{}
    }
    
    type F struct {
    	a int32
    	b int32
    	c struct{}
    }
    
    func main() {
    	fmt.Println(unsafe.Sizeof(C{})) // 16
    	fmt.Println(unsafe.Sizeof(D{})) // 16
    	fmt.Println(unsafe.Sizeof(E{})) // 24
      fmt.Println(unsafe.Sizeof(F{})) // 12
    }
    

    总结

    本篇文章我们一起学习了Go 语言中的内存对齐,主要内容如下:

    • unsafe.Sizeof(x) 返回了变量x的内存占用大小
    • 两个结构体,即使包含变量类型的数量相同,但是位置不同,占用的内存大小也不同,由此引出了内存对齐
    • 内存对齐包含成员对齐和整体对齐,与 unsafe.AlignOf(x) 息息相关
    • 空结构体作为成员变量时,是否占用内存和所处位置有关
    • 在实际开发中,我们可以通过调整变量位置,优化内存占用(一般按照变量内存大小顺序排列,整体占用内存更小)

    更多

    个人博客: https://lifelmy.github.io/

    微信公众号:漫漫Coding路

    展开全文
  • C/C++ 内存对齐

    千次阅读 2022-03-28 13:59:32
    什么是内存对齐 为了提高程序的性能,数据结构应该尽可能地在自然边界上对齐。 为什么需要内存对齐 1、便于移植:不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的...
  • 但实际情况是在访问特定类型变量的时候经常在特定的内存地址访问,这就需要各种类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,这就是对齐对齐的作用和原因:各个硬件平台对存储空间的处理...
  • 为了优化CPU访问和优化内存,减少内存碎片,编译器对内存对齐制定了一些规则。但是,不同的编译器可能有不同的实现,本文只针对VC++编译器,这里使用的IDE是VS2012。 #pragma pack()是一个预处理,表示内存对齐。...
  • C++内存对齐

    2022-05-10 17:59:42
    C++内存对齐
  • C++ 内存分配与内存对齐

    千次阅读 2021-11-10 14:29:38
    一、C++程序内存分配 C/C++程序编译时内存分为5大存储区 栈区,由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。通俗来讲就是函 数中的变量参数等等,即{}中的内容。...
  • C语言结构体内存对齐

    2022-02-23 09:45:38
    结构体内存对齐的意义代码演示及运行结果分析代码演示运行结果展示运行结果分析。gcc对结构体对齐的支持 结构体内存对齐的意义 (1)结构体中元素对齐访问主要原因是为了配合硬件,也就是说硬件本身有物理上的限制,...
  • 这里写自定义目录标题Go struct 内存对齐举个例子原理术语为什么要关心对齐为什么要做内存对齐数据结构对齐 Go struct 内存对齐 你好! 这是你第一次使用 Markdown编辑器 所展示的欢迎页。如果你想学习如何使用...
  • 关于内存对齐

    2021-02-28 08:52:14
    曾经接手一个网络视频监控程序,主要是上层软件通过发送控制指令获取网络视频板的视频数据和控制网络视频板,这应该是一个...我是按照视频的操作指令封装的数据包,内存显示数据包的内容是没有错误的,就是一个晚上,我...
  • C++ 结构体内存对齐

    2021-07-30 14:29:35
    血的教训?面试被问到结构体内存对齐,哑口无言!
  • 【C语言系列】-结构体中的内存对齐

    千次阅读 多人点赞 2022-01-25 12:05:19
    搞定内存对齐,你只需要~
  • golang内存对齐

    2021-11-15 15:31:42
    内存对齐 什么是内存对齐 为保证程序顺利高效的运行,编译器会把各种类型的数据安排到合适的地址,并占用合适的长度,这就是内存对齐。 每种类型的对齐值就是它的对齐边界,内存对齐要求数据存储地址以及占用的字节...
  • 内存对齐3.1 结构体成员默认内存对齐3.2 不同架构内存对齐方式3.3 小试牛刀3.3.1 前置填充3.3.2 中间填充3.3.3 尾随填充 1. 同个结构体占用内存可变化      在 C语言之结构体 章节里,对struct的功能和使用进行...
  • 结构体内存对齐

    多人点赞 2022-01-11 19:36:24
    1、结构体内存对齐是指当我们创建一个结构体变量时,会向内存申请所需的空间,用来存储结构体成员的内容。我们可以将其理解为结构体成员会按照特定的规则来存储数据内容。 2、结构体的对齐规则 (1)第一个成员在...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 184,857
精华内容 73,942
关键字:

内存对齐

友情链接: 时滞系统资料.rar