C/C++/Linux

内存对齐

2019-08-05  本文已影响0人  ninedreams

简介

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

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的性能?

上一篇下一篇

猜你喜欢

热点阅读