Data structure alignment (数据结构对齐 / 内存对齐)

2023-05-16

开篇的话

  1. 在比较老的编译器里,如果没有对变量取地址的操作,那么有些局部变量是通过寄存器保存的,不占栈上内存,根本不存在内存中如何排列的问题,比如TurboC 2.0这种。

  2. 在一些较新的编译器里,局部变量排列顺序并不是从上到下一次排列,有些是根据使用频率来排列的,这样可以降低cache的miss率,所以怎么排列完全根据使用频率。

  3. 大部分主流编译器的局部变量地址确实是从下到上,但也有反过来的。从下到上排列的好处之一是编译器处理一个函数的时候,可以动态增长栈的长度,不需要先判断局部变量有多少个。

  4. 还有一些非主流编译器会调整变量的排列次序,使得其在各个变量基本对齐的情况下占用的栈空间最小,对于一些嵌入式设备来说,非常有用。

前言

数据结构对齐是指在计算机内存中排列和访问数据的方式。它包含三个独立但相关的问题:数据对齐(data alignment)数据结构填充( data structure padding)打包(packing.)

当数据自然对齐时,现代计算机硬件中的CPU最有效地执行对内存的读写操作,这通常意味着数据的内存地址是数据大小的倍数。

  1. 读/写总是从word_size的倍数的地址开始的。

  2. 读/写的长度总是word_size的倍数。
    例如,在32位体系结构中,如果数据存储在四个连续字节中并且第一个字节位于4字节边界上,则可以对齐数据。

数据对齐是指根据元素的自然对齐来对齐元素。为了确保自然对齐,可能有必要在结构元素之间或结构的最后一个元素之后插入一些填充。

例如,在32位计算机上,包含16位值和32位值的数据结构可以在16位值和32位值之间具有16位填充以对齐32位在32位边界上的值。,或者,可以打包结构,省略填充 可能会导致访问速度变慢,但其优点是使用的内存只有原来的一半。

在一台64位的计算机上,读取一块1k的内存,CPU需要进行的读取操作次数是:

  • 1 k = 1024 b y t e = 1024 ∗ 8 b i t s 1k = 1024 byte = 1024 * 8 bits 1k=1024byte=10248bits
  • 1024 ∗ 8 b i t s 64 b i t s / t i m e = 128 t i m e \frac {1024 \ast 8 bits}{ 64 bits/time } = 128 time 64bits/time10248bits=128time

原因

CPU一次通过单个存储字访问存储器。只要存储字大小至少与计算机支持的最大原始数据类型一样大,对齐的访问将始终访问单个存储字。对于未对齐的数据访问,情况可能并非如此。

如果数据中的最高字节和最低字节不在同一存储字中,则计算机必须将数据访问分为多个存储访问。这需要很多复杂的电路来生成内存访问并进行协调。为了处理存储字位于不同存储页面中的情况,处理器必须在执行指令之前验证两个页面均存在,或者必须能够在指令执行过程中处理任何存储器访问中的TLB丢失或页面错误。

一些处理器设计故意避免引入这种复杂性,而是在内存访问未对齐的情况下产生替代行为。例如,ARMv6 ISA之前的ARM体系结构的实现要求对所有多字节加载和存储指令进行强制性对齐内存访问。根据发出的特定指令,尝试进行未对齐访问的结果可能是舍入有问题地址的最低有效位,从而将其转换为对齐访问(有时会有其他警告),或者引发MMU异常( (如果存在MMU硬件),或静默产生其他潜在的不可预测的结果。从ARMv6架构开始,添加了支持以处理许多(但不一定是所有)情况下的未对齐访问。

当访问单个存储字时,该操作是原子的,即一次读取或写入整个存储字,并且其他设备必须等待读取或写入操作完成才能访问它。对于未对齐访问多个存储字可能不是正确的,例如,第一个字可能被一个设备读取,两个字都被另一个设备读取,然后第二个字被第一个设备读取,因此读取的值都不是原始值也不更新值。尽管此类故障很少见,但很难识别。

举例

内存地址不对齐,会引起什么样的问题呢?用一个例子来说明:

  • 假定在一台32位机器上,有一个整型变量i的地址是34 ,那i存储在内存的34、35、36、37地址;

  • 为了把这个变量从内存读进CPU,由于计算机从内存读取数据的天性(第一点,读/写总是从word_size的倍数的地址开始的),需要两次读取(第一次从32开始读32 33 34 35,第二次从36开始读36 37 38 39),然后把第一次读取的后两个字节(34 35)抽取出来,把第二次读取的前面两个字节(36 37)抽取出来拼到一起组成变量i:

在这里插入图片描述
一个int变量为4bytes,即32位,从CPU一次可以读取的内存块长度来看,本可以一次读完;但是因为这个变量的内存块地址没有对齐,将导致本来一个read指令就能完成的读取操作,需要两次read外加其它复杂的抽取拼接计算,从而大大地降低了性能。

看一个实例:

环境:64位系统,Xcode的clang编译器

在这里插入图片描述

  1. 为什么ac地址不是间隔4呢?
    答: clang编译器按照定义的顺序分配地址,地址是从上往下分配的(由高到低)。所以首先设置好c的地址。接着,a的地址必须是4的整数倍,从c的地址0xef往下,第一个4的整数倍是0xec,但是它们之间只有3个空位,所以没法用。于是a必须再往下找下一个4的整数倍,即0xe8。

    注:X86是小端,栈地址的生长方式 (从高地址到低地址) – 即先分配的变量存在高地址,后分配的存在低地址,栈结构中高地址的方向叫做栈底,栈的访问方式是先进后出

  2. 第一个4的整数倍是0xec,那么为什么整数不是从0xec往小地址内存存呢,如0xeb,0xea, 0xe9, 0xe8呢,而是从0xec, 0xed, 0xef存呢?
    答:小端模式,一个局部变量(栈空间)的地址,就是它的占据的几个位置里面值最小的那个(指针指向最低地址的字节)

结构的典型对齐方式

  • 数组 :按照基本数据类型对齐,第一个对齐了后面的自然也就对齐了。
  • 联合 :按其包含的长度最大的数据类型对齐。
  • 结构体: 结构体中每个数据类型都要对齐。

结构字节对齐的原则主要有:

  1. 数据类型自身的对齐值:char型数据自身对齐值为1字节,short型数据为2字节,int/float型为4字节,double型为8字节 …(操作系统不同可能由偏差)

  2. 结构体或类的自身对齐值:其成员中自身对齐值最大的那个值。(结构体的每一个成员相对结构体首地址的偏移量应该是对其参数的整数倍,如果不满足则补足前面的字节使其满足)

  3. 指定对齐值:#pragma pack (value)时的指定对齐值value。

  4. 数据成员、结构体和类的有效对齐值:自身对齐值和指定对齐值中较小者,即有效对齐值=min{自身对齐值,当前指定的pack值}。

我们直接看例子吧:

typedef struct node1 {
} S1;
typedef struct node2 {  
    int a;
    char b;
    short c;
} S2;
typedef struct node3 {
    char a;
    int b;
    short c;
} S3;
typedef struct node4 {
    int a;
    short b;
    static int c; //静态变量单独存放在静态数据区
} S4;
typedef struct node5 {
    bool a;
    S1 b;
    short c;
} S5;
typedef struct node6 {
    bool a;
    S2 b;
    int c;
} S6;
typedef struct node7 {
    bool a;
    S2 b;
    double d;
    int c;
} S7;
typedef struct node8 {
    bool a;
    S2 b;
    char* c;
} S8;
  1. node1为一个空结构体,在C中空结构体的大小为0字节,在C++中空结构体的大小为1字节。
  2. node2的内存结构:(4 — 1 — 1(补) — 2),总大小为8字节(结构体的每一个成员相对结构体首地址的偏移量应该是对其参数的整数倍)。
  3. node3的内存结构:(1 — 3(补) — 4 — 4),总大小为12字节(结构体的每一个成员相对结构体首地址的偏移量应该是对其参数的整数倍)。
  4. node4的内存结构:(4 — 2 — 2(补)),总大小为8字节,注意静态变量被分配到静态数据区,不在sizeof计算的范围内。
  5. node5的内存结构:(1 — 1 — 2),总大小为4字节。
  6. node6的内存结构:(1 — 3(补) — 8 — 4),总大小为16字节,注意结构体变量的对齐参数的计算。
  7. node7的内存结构:(1 — 3(补)— 8 — 4(补) — 8 — 4 — 4(补)),总大小为32字节。
  8. node8的内存结构:(1 — 3(补) — 8 — 4),总大小为16字节。

详细分析一下node7,其余的也类似:

#pragma pack(n)为8对于a变量,其对齐参数为1,此时offset=0,可以被1整除,因此为其分配1字节空间;

对于b变量,其对齐参数为4(s2结构体的成员变量最大对齐参数为int => 4),此时offset=1,不能被4整除,因此填充3字节后为其分配8字节空间;

对于d变量,其对齐参数为8,此时offset=12,不能被8整除,因此填充4字节后为其分配8字节空间。

对于c变量,其对齐参数为4,此时offset=24,可以被4整除,因此为其分配4字节空间。此时所有变量都分配完,但此时offset=28,不能被最大对齐参数8整除,因此填充4字节使其可以被8整除。所以最后node7的大小为32字节。

强制指定对齐大小

在缺省情况下,C编译器为每一个变量或是数据单元按其自然对界条件分配空间。一般地,可以通过下面的方法来改变缺省的对界条件:

  • 使用伪指令#pragma pack (n),C编译器将按照n个字节对齐。
  • 使用伪指令#pragma pack (),取消自定义字节对齐方式。

gcc里还可以使用__attribute__关键字来声明数据类型的对齐方式,优先级高于#pragma预编译指令。

  • __attribute((aligned (n))),让所作用的结构成员对齐在n字节自然边界上。如果结构中有成员的长度大于n,则按照最大成员的长度来对齐。
  • attribute ((packed)),取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐。

1. __attribute__方式

 struct stu{
   char sex;
   int length;
   char name[10];
  }__attribute__ ((aligned (1))); 
  
  struct stu my_stu;

此时,sizeof(my_stu)可以得到大小为15。上面的定义等同于:

  struct stu{
   char sex;
   int length;
   char name[10];
  }__attribute__ ((packed)); 
  struct stu my_stu;
  • 当aligned作用于变量时,其作用是告诉编译器为变量分配内存的时候,要分配在指定对其的内存上,作用于变量之上不会改变变量的大小。
    例如:int a attribute((aligned(16)));该变量a的内存起始地址为16的倍数。
  • 当aligned作用于类型时,其作用是告诉编译器该类型声明的所有变量都要分配在指定对齐的内存上。当该属性作用于结构体声明时可能会改变结构体的大小。

2. pragma方式

声明#pragma可以设置对齐参数的数值,缺省是8字节:

#pragma pack (n) 				//作用:C编译器将按照n个字节对齐。
#pragma pack ()               	//作用:取消自定义字节对齐方式。
#pragma pack (push,1)     		//作用:是指把原来对齐方式设置压栈,并设新的对齐方式设置为一个字节对齐
#pragma pack(pop)            	//作用:恢复对齐状态
#pragma pack(push)				 //保存对齐状态
#pragma pack(4)					//设定为4字节对齐 相当于 #pragma  pack (push,4)  

3. aligned和pack的主要区别

  • pack作用于结构体或类的定义,而aligned既可以作用于结构体或类的定义,也可以作用于变量的声明。
  • pack的作用是改变结构体或类中成员变量的布局规则,而aligned只是建议编译器对指定变量或指定类型的变量分配内存时的规则。
  • pack可以压缩变量所占内存的空间
  • align可以指定变量在内存的对其规则,而pack不可以。
  • 若某一个结构体的默认pack为n,pack指定的对齐规则m大于n,则该pack忽略。若aligned指定的对齐规则s大于n,则此时结构体的大小一定为s的整数倍。
  • aligned和pack指定规则时都必须为2的n次幂。


参考资料

  • Wiki - Data structure alignment
  • 知乎 - C语言中连续定义两个变量,为什么地址是这样的?
  • 刘煌旭 - 深入理解内存对齐
  • linuxsong - C/C++字节对齐详解
  • sczyh30 - C/C++ 结构体字节对齐
  • 魏传柳 - C语言pack与aligned的区别
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Data structure alignment (数据结构对齐 / 内存对齐) 的相关文章

  • C_带参数的宏定义

    C 带参数的宏定义 xff23 语言允许宏带有参数 在宏定义中的参数称为形式参数 xff0c 在宏调用中的参数称为实际参数 对带参数的宏 xff0c 在调用中 xff0c 不仅要宏展开 xff0c 而且要用实参去代换形参 带参宏定义的一般形
  • 十进制数转换成十六进制数~C语言

    include lt stdio h gt 下面将整数a转换成十六进制输出的字符串 原理 xff1a 1 xff0c 首先知道0b100000 61 0b10000 2 61 0b1000 2 61 0b100 2 61 0b10 2 利用
  • Qt实现线程安全的单例模式

    实现方式 1 实现单例 把类的构造函数 拷贝构造函数 赋值操作符定义为private的 xff1b 把获取单例的接口和唯一的实例指针定义为static的 xff0c 不需要实例化 xff0c 直接通过类名即可访问 2 支持多线程 采用双重校
  • 文本文件和二进制文件的差异和区别

    广义上的二进制文件包括文本文件 xff0c 这里讨论的是狭义上的二进制文件与文本文件的比较 xff1a 能存储的数据类型不同 文本文件只能存储char型字符变量 二进制文件可以存储char int short long float 各种变量
  • Qt实现记录日志文件log

    概述 Qt有两种实现记录日志的方式 xff0c 第一种是安装自定义的Qt消息处理程序 xff0c 自动输出程序产生的调试消息 警告 关键和致命错误消息的函数 xff1b 第二种是自定义一个类 xff0c 可以在程序指定位置打印输出指定的内容
  • Qt在linux环境下调用动态库,pro工程文件加载库和QLibrary加载库两种方式

    QT调用动态库 xff0c 在编译时和运行时的方式不同 xff0c 编译时可在pro文件加载或使用QLibrary类加载 xff1b 运行时依赖环境变量 xff0c windows下直接把动态库拷贝到可执行文件目录即可 xff0c linu
  • linux下QT发布程序双击打不开解决方法

    现象 Qt开发的程序 xff0c 使用 终端可以打开 xff0c 双击却打不开 阶段一 右键可执行程序 xff0c 选择属性 xff0c 可执行程序类型如果是 application x sharedlib xff0c 在QT的pro文件添
  • Qt发起http请求,get和post方式,并接收响应数据

    目录 Qt发起http请求get xff0c 异步接收Qt发起http请求post xff0c 异步接收Qt发起http请求get和post xff0c 收发同步http下载网络图片 Qt发起http请求get xff0c 异步接收 get
  • QT实现浏览器访问网页,使用QWebEngineView

    支持访问网页 xff0c 前进 后退 刷新 xff0c 点击超链接自动跳转 xff0c 获取网页鼠标事件 xff0c 重新编译QWebEngineView库后还可以支持播放mp4等视频 xff1b Qt在debug模式运行有时访问网页很卡
  • Qt程序打包成安装包exe

    本章介绍把Qt开发的程序打包成安装包的方法 xff0c 程序打包成install exe xff0c 可双击安装 xff0c 有默认安装路径 xff0c 也可以选择安装目录 xff0c 自动生成桌面快捷方式和开始菜单选项 xff0c 可以在
  • C/C++socket网络编程

    目录 tcp和udp通信流程图socket函数bind函数listen函数accept函数connect函数recv recvfrom read函数send write sendto sendmsg函数close shutdown函数hto
  • OSPF路由协议解释

    目录 OSPF路由协议OSPF数据包类型OSPF邻区状态OSPF的邻接关系建立过程 路由名词解释OSPF开源项目 OSPF路由协议 OSPF简介 1 xff08 Open Shortest Path First xff09 xff0c 开放
  • redis服务搭建,C++实现redis客户端,redis远程可视化工具

    目录 redis简介redis服务搭建redis常用命令C 43 43 实现redis客户端redis远程可视化工具 Another Redis DeskTop Manager redis简介 官方网址 xff1a https redis
  • Dijkstra算法图解,C++实现Dijkstra算法

    目录 Dijkstra算法简介数据结构抽象初始化开始计算第一轮计算第二轮计算第三轮计算第四轮计算算法总结 C 43 43 实现Dijkstra算法 Dijkstra算法简介 Dijkstra算法计算是从一个顶点到其余各顶点的最短路径算法 x
  • python实现ID3决策树分类算法

    所有的分类与回归算法中心思想大致是一样的 xff0c 那就是根据现有带标签的数据集训练一个分类器模型 xff0c 然后对待未知的样本 xff0c 根据训练好的分类模型来判定它属于哪个类 分类与回归的区别在我看来就是标签连续与否的区别 xff
  • Multiclass SVM(多类别SVM分类)关于其 loss function 的求导

    这两天学习cs231n的课程 xff0c 顺便做一做该课程配套的作业 在assignment1中有遇到用multiclass SVM来对cifar10进行分类的问题 其中 xff0c 为了进行训练 xff0c 需要计算loss 和相关梯度
  • 解决Ubuntu14.04 下不显示wifi的情况

    本人电脑安装完Ubuntu14 04后 xff0c 发现只能连接有线网 xff0c 而在右上角的网络下拉菜单中找不到wifi 网上的大多数教程大概为以下两类 xff1a 一类是 sudo apt get update sudo apt ge
  • Ubuntu系统下连接SJTU的校园wifi

    本人目前系统是Ubuntu 16 04 xff0c 但是14 04下按照这个方法也能成功连接上 xff08 亲测有效 xff09 如下方法应该能解决linux系统无法连接SJTU校园网的问题 首先 xff0c 点击连接SJTU wifi x
  • VS 代码对齐、折叠以及其他常用快捷键整理

    VS 代码对齐 折叠以及其他常用快捷键整理 Ctrl 43 M 43 O 折叠所有方法 Ctrl 43 M 43 M 折叠或者展开当前方法 Ctrl 43 M 43 L 展开所有方法 Ctrl 43 K xff0c Ctrl 43 D 61
  • Postman使用详解

    一 Postman背景介绍 用户在开发或者调试网络程序或者是网页B S模式的程序的时候是需要一些方法来跟踪网页请求的 xff0c 用户可以使用一些网络的监视工具比如著名的Firebug等网页调试工具 今天给大家介绍的这款网页调试工具不仅可以

随机推荐

  • 玩转51单片机 (一):Keil4中多文件项目中全局变量、头文件和源文件

    首先 xff0c 工程文件夹下有src inc 和 project三个文件夹 xff0c 分别存放源文件 头文件和工程文件 然后 xff0c 将所有源文件 头文件都添加到项目里面 xff0c 并将头文件目录添加到魔术棒下C51选项 的 In
  • Vins-Fusion工控机运行Debug

    一 问题记录 在自己的笔记本上运行Vins Fusion效果OK xff0c 但在工控机上运行出现静止状态轨迹飘飞 xff0c 笔记本上和工控机上都是ubantu 18 04系统 xff0c 摄像头硬件均为ZED2i xff0c 配置文件相
  • xavier安装torch-gpu

    1 查看xavier安装的jetpack版本 xff1a sudo apt show nvidia jetpack 一般情况下都是462的 2 查看cuda版本 xff1a cat usr local cuda version txt 3
  • 保存图像(cv::imwrite)

    保存图像文件道指定目录路径 只有8位 xff0c 16位的PNG xff0c JPG xff0c Tiffy文件格式而且时单通道或者三通道的BGR的图像才通过这种方式保存 保存PNG格式的时候可以保存透明通道的图片 可以指定压缩参数 inc
  • 解决 ssh: Could not resolve hostname \342\200\223t: Name or service not known

    问题 xff1a 在配置github时输入以下内容时报错 xff1a ssh T git 64 github span class token punctuation span com 方法 xff1a 将上述内容换为以下内容 ssh sp
  • python实现随机森林

    定义 xff1a 随机森林指的是利用多棵决策树对样本进行训练并预测的一种分类器 可回归可分类 所以随机森林是基于多颗决策树的一种集成学习算法 xff0c 常见的决策树算法主要有以下几种 xff1a 1 ID3 xff1a 使用信息增益g D
  • 从Github上下载文件的方法汇总

    前言 对于程序猿来说 xff0c Github简直就是个宝藏 xff0c 里面有世界各地大神的代码 xff0c 可以放心安全高效的食用 xff0c 本文整理了Github文件的一些下载方法 xff0c 仅供参考 1 通过git命令行 特点
  • fatal error: Python.h: No such file or directory #include “Python.h“

    在TX2上安装h5py时 xff0c pypi org没有对应的轮子 xff0c 需要自己用源码安装 xff0c 源码中涉及到C语言 xff0c 所以需要编译 xff0c 然而在编译的过程中报错 fatal error span class
  • 基于Python的ZED2教程 0.ZED2介绍

    本文主要介绍了ZED2的基本简介和硬件配置 基本简介 ZED2双目深度传感立体相机是位于美国旧金山Stereo labs公司制作的一款产品 xff0c 其与Kinect相机等流行的深度图像原理不同 xff0c 该深度相机的深度计算是通过双目
  • 基于Python的ZED2教程 1.打开ZED2

    本教程简单的对ZED相机进行了配置和打开 xff0c 然后打印出ZED 相机的串口号 xff0c 接着关闭相机 准备工作 由于ZED SDK在GPU上运算 xff0c 因此需要先在 nvidia com下载最新版的Cuda 然后 xff0c
  • 2.Open3D教程——文件读取和保存

    文件读取和保存 本教程演示了Open3D如何读写基本数据结构 1 点云 下面的代码读取和写入点云 span class token keyword print span span class token punctuation span s
  • 7.Open3D教程——表面重建

    在许多情况下 xff0c 我们希望生成密集的三维几何体 xff0c 即三角形网格 然而 xff0c 从多视点立体方法 xff0c 或深度传感器 xff0c 我们只能获得一个非结构化的点云 为了从非结构化输入中得到三角形网格 xff0c 我们
  • PASCAL VOC数据集

    一 简介 PASCAL pattern analysis statistical modelling and computational learning VOC visual object classes 该挑战赛的竞赛项目主要包括 图像
  • ubuntu python 通过奥比中光摄像头获取深度图片和彩色图片

    1 依赖 安装Openni Openni下载Openni添加至环境 xff08 要通过全局变量找到Openni头文件和库 xff09 安装primesense和openni pip install primesense pip instal
  • ROS tf使用报错:ImportError: dynamic module does not define module export function (PyInit__tf2)

    1 报错内容 Traceback span class token punctuation span most recent call last span class token punctuation span File span cla
  • ubuntu cuda cudnn tensorRT的卸载和安装

    1 安装显卡驱动 显卡安装教程 查看N卡驱动支持的最高cuda版本 nvidia smi 2 卸载 span class token function sudo span span class token function apt get
  • 初识VSCode

    Visual Studio Code xff08 以下简称vscode xff09 是一个轻量且强大的代码编辑器 xff0c 跨平台支持Windows xff0c Mac OS X和Linux 内置JavaScript TypeScript
  • Modbus通信及数据存储读取

    1 存储区代号 代码号功能1区输入线圈0区输出线圈3区输入寄存器4区输出寄存器 2 功能码 代码功能0x01读取输出线圈0x02读取输入线圈0x03读取输出寄存器0x04读取输入寄存器0x05写入单个线圈0x06写入单个寄存器0x0F写入多
  • 着色器语言 GLSL (opengl-shader-language)入门大全

    GLSL 中文手册 基本类型 类型说明void空类型 即不返回任何值bool布尔类型 true falseint带符号的整数 signed integerfloat带符号的浮点数 floating scalarvec2 vec3 vec4n
  • Data structure alignment (数据结构对齐 / 内存对齐)

    开篇的话 在比较老的编译器里 xff0c 如果没有对变量取地址的操作 xff0c 那么有些局部变量是通过寄存器保存的 xff0c 不占栈上内存 xff0c 根本不存在内存中如何排列的问题 xff0c 比如TurboC 2 0这种 在一些较新