内存对齐

简介

在传统体系的计算机中,我们知道CPU的运算速度是最快的,也是最昂贵的部件。其次是寄存器,加速优化与内存的读写速度,寄存器的速度也是快于内存。然后是多级缓存。之后就进入到内存,内存的读取写入速度要远慢于CPU的速度,价格上也是如此。内存对齐是为了降低cpu访问内存的次数,更高效的使用CPU。CPU读取内存是高耗时的指令,所以内存对齐,是在内存的使用量和CPU计算上做的一种居中的优化策略。这种策略是由编译器决和CPU共同决定,并且程序员可以设置对齐的长度。
很多 CPU(如基于 Alpha,IA-64,MIPS,和 SuperH 体系的)拒绝读取未对齐数据。当一个程序要求这些 CPU 读取未对齐数据时,这时 CPU 会进入异常处理状态并且通知程序不能继续执行。举个例子,在 ARM 硬件平台上,当操作系统被要求存取一个未对齐数据时会默认给应用程序抛出硬件异常。所以,如果编译器不进行内存对齐,那在很多平台的上的开发将难以进行。

  • 总结一句话:内存对齐是以牺牲内存空间来提高CPU的性能的空间换时间的策略。

sizeof关键字

先看一段代码:

#include <iostream>

int main(int argc, char** argv) {
    std::cout << sizeof(long long) << std::endl;
    std::cout << sizeof(long) << std::endl;
    std::cout << sizeof(unsigned long) << std::endl;
    std::cout << sizeof(int) << std::endl;
    std::cout << sizeof(unsigned int) << std::endl;
    std::cout << sizeof(short) << std::endl;
    std::cout << sizeof(unsigned short) << std::endl;
    std::cout << sizeof(char) << std::endl;
    std::cout << sizeof(float) << std::endl;
    std::cout << sizeof(double) << std::endl;
    return 0;
}

在我的环境下gcc version 6.5.0 (MacPorts gcc6 6.5.0_1)运行结果如下:

image.png

我的机器是64位,但是如果是32位的,运行的结果有些就会不一样了,比如sizeof(long)的结果为4,但是sizeof(char)的结果仍然为1。
上面的所有结果都是基础的类型,没有指针类型,下面看一下指针类型:

int main(int argc, char** argv) {
    std::cout << sizeof(double*) << std::endl;
    std::cout << sizeof(int*) << std::endl;
    std::cout << sizeof(short*) << std::endl;
    std::cout << sizeof(char*) << std::endl;
    std::cout << sizeof(float*) << std::endl;
    std::cout << sizeof(long long*) << std::endl;
    std::cout << sizeof(long*) << std::endl;
    return 0;
}

在我的64位机器上输出结果如下:

image.png

所有的结果都是8,这是因为sizeof对他们取的是指针所占的内存大小,而不是具体的类型占用内存大小。所以不论是什么类型的指针,sizeof出来的结果都是8。当然如果是在32位机器上的话,结果都是4,因为32位机器32bit位足以表示内存地址,那么就不需要使用更多的内存去存储指针。

数据对齐

数据成员对齐

首先我们先看一段演示的代码:

#include <iostream>
struct A {
    int a;
    char b;
    long c;
    double* d;
};
struct B {
    double* a;
    int b;
    long c;
    char d;
};
int main(int argc, char** argv) {
    std::cout << sizeof(A) << std::endl;
    std::cout << sizeof(B) << std::endl;
    return 0;
}

在我的机器上输出的结果为:

image.png

这里就是内存对齐导致的两个结构体,只是内部的元素位置变换,而占有的内存空间缺不一样。对于我的机器,64位系统,gcc version 6.5.0 (MacPorts gcc6 6.5.0_1),默认是以8字节对齐。首先我们知道结构体分配内存时,按照声明的变量顺序来存储数据。对于A结构体,首先是分配int a的空间,分配4字节,然后再是char bchar b只占用一个字节,此时给它分配内存时,就会在int a后面空余的4个字节从第一个字节开始分配给它。于是这个char b占用了4个字节,当然后面的3个字节不属于它,但是也并没有使用,填充空字节。因为接下来的是long cdouble* d这两个都是8字节的内存空间,于是总共分配的内存空间就是4+1+3+8+8=24字节。同理可得到结构体B,8+4+4+8+1+7=32字节。所以我们在定义结构体时,稍微注意一下声明的顺序,就可以节约许多的内存。下面我们看一下内存的地址是否与我们分析的一致:

int main(int argc, char** argv) {
    A a;
    std::cout << &a << std::endl;
    std::cout << &a.a << std::endl;
    std::cout << static_cast<void*>(&a.b) << std::endl;
    std::cout << &a.c << std::endl;
    std::cout << &a.d << std::endl;
    return 0;
}

输出结果:


image.png

结果中我们可以看到:

  • 结构体的地址就是结构体第一个数据的地址
  • 内存占用与分析的一致

数据对齐的规则与对齐系数

  1. #pragma pack(n)
    这个参数表示指定的数值n和这个数据成员自身长度中较小那个的整数倍,这个数据作为在内存中的偏移。out = N * min(n, min(struct))

  2. 数据成员对齐规则
    structunion的数据成员,第一个数据成员放在偏移为 0 的地方(偏移起始的地址为结构体的的地址),以后每个数据成员的偏移为预先指定的数值和这个数据成员自身长度中较小那个的整数倍,现在的默认64位机器为8字节。

  3. 数据成员为结构体
    如果结构体的数据成员还为结构体,则该数据成员的“自身长度”为其内部最大元素的大小。如:struct a 里存有 struct b,b 里有char,int,double等元素,那 b “自身长度”为 8。len(a) = len(max(b.element))

  4. 结构体的整体对齐规则
    在数据成员按照2号规则完成各自对齐之后,结构体本身也要进行对齐。对齐会将结构体的大小增加为#pragma pack(n)指定的数值和结构体最大数据成员长度中,两个数字中较小那个数字的整数倍。out = N * min(n, max(struct))

下面是验证的代码,我们将上面的代码设置上#pragma pack(4):

#include <iostream>
#pragma pack(4)
struct A {
    int a;
    char b;
    long c;
    double* d;
};
struct B {
    double* a;
    int b;
    long c;
    char d;
};
int main(int argc, char** argv) {
    std::cout << sizeof(A) << std::endl;
    std::cout << sizeof(B) << std::endl;
    return 0;
}

运行结果如下:


image.png

为什么需要内存对齐

我们在上面已经讲了,内存对齐是一种优化CPU性能的方法,那么为什么CPU会需要内存对齐来优化性能呢?没有内存对齐的数据为什么会大大降低CPU的性能?

  • 字节对齐取数据
    首先假设各位都是学习过微机原理或者是懂一些这方面知识。我们知道计算机中虚拟内存地址对应于实际的物理地址,能够保证CPU取到每一个字节内存的物理地址,也就是说每一个内存的字节都会有地址。但是多数CPU并不是以字节为单位去取物理内存上面的数据,假如CPU需要取一个8字节的数据到CPU中运算,那么取数据将会花费8个取内存数据的指令周期,这还不包括地址偏移和数据合并的指令周期。实际中,CPU的运算速度是非常的快,但是时间都花费在了取内存数据上,这是对CPU的浪费。所以CPU一般会以2/4/8/16/32字节为单位来进行存取操作。我们将上述这些存取单位称为内存存取粒度,这样假设8字节的数据,那么使用内存存取粒度为8的话,取一次内存数据就完成了。
  • 硬件设计
    最初的 68000 处理器的存取粒度是双字节,没有应对非对齐内存地址的电路系统。当遇到非对齐内存地址的存取时,它将抛出一个异常。随后的 680x0 系列,像 68020,放宽了这个的限制,支持了非对齐内存地址存取的相关操作。这解释了为什么一些在 68020 上正常运行的旧软件会在 68000 上崩溃,当然这也跟编译存在一定的关系。
    处理器都是使用有限的晶体管来完成工作。支持非对齐内存地址的存取操作会消减“晶体管预算”,这些晶体管原本可以用来提升其他模块的速度或者增加新的功能。现在内存对齐基本已经成了一个约定,如果在编译时,没有内存对齐,而CPU也不支持非内存对齐,那么抛出异常交给操作系统取处理。一般来说,硬件的解决方案会比软件解决方案快非常多,所以现在编译器默认都是有内存对齐的。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,509评论 6 504
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,806评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,875评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,441评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,488评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,365评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,190评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,062评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,500评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,706评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,834评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,559评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,167评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,779评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,912评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,958评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,779评论 2 354

推荐阅读更多精彩内容

  • 首先通过一段代码来描述内存对齐的现象。 上述代码打印出来的结果为:12,8 为什么相同的结构体,只是交换了变量 a...
    xuyafei86阅读 2,989评论 2 15
  • 源网址[英文] github上有大神翻译了一篇内存对齐的英文文献,我复现了一下过程; 发现其中有个地方有出入(st...
    十曰立阅读 1,196评论 0 3
  • 查询内网中关于内存对齐的资料发现,它们往往只谈论一个层面的问题,而不涉及或稍微涉及更高或更低层面的问题;而这对于喜...
    丹丘生___阅读 1,286评论 0 3
  • unsafe 包简单说明 unsafe,顾名思义,是不安全的,Go定义这个包名也是这个意思,让我们尽可能的不要使用...
    Gopherzhang阅读 1,510评论 8 3
  • 原文地址:在 Go 中恰到好处的内存对齐 问题 在开始之前,希望你计算一下 Part1 共占用的大小是多少呢? 输...
    EDDYCJY阅读 1,116评论 1 11