内存对齐——知识点探究

2020-05-07  本文已影响0人  哦小小树

以下问题探究点在64位操作系统下进行探究。

0x01什么是内存对齐

编译器为程序中的每个数据单元安排在合适的位置上,来实现CPU以极少的次数获取到内存中的数据。【这是编译器干的活】

上例子:

// C语言示例
typedef struct {
    int a;      // 4 Byte
    double b;   // 8 Byte
    short c;    // 2 Byte
}AStruct;

typedef struct {
    short b;    // 2 Byte
    int a;      // 4 Byte
    double c;   // 8 Byte
}BStruct;

// 测试用例
AStruct a = {1,2.0f,3};
BStruct b = {1,2,3.0f};
        
NSLog(@"%d-%.2f-%d-size:%d",a.a,a.b,a.c,sizeof(a));
NSLog(@"%d-%d-%.2f-size:%d",b.a,b.b,b.c,sizeof(b));


# TestMemoryAlign[4069:228467] 1-2.00-3-size:24
# TestMemoryAlign[4069:228467] 2-1-3.00-size:16

发现了点什么?同一个结构体,为啥sizeof查出来的大小不一样呢?

这就是编译器在编译的时候做了内存对齐导致不同的效果。

对于我们来说,内存对齐基本是透明的,这是编译器该干的活,编译器为程序中的每个数据单元安排在合适的位置上,从而导致相同的变量,不同声明顺序的结构体大小不同。


0x02 为什么要内存对齐

#pragma pack(1)     # 不使用内存对齐 后面会解释
typedef struct {
    int a;      // 4 Byte
    double b;   // 8 Byte
    short c;    // 2 Byte
}AStruct;

AStruct a = {1,2.0f,3};
# TestMemoryAlign[4136:236686] 1-2.00-3-size:14
# 假设 a从0开始,当然如果a不从0开始会更复杂
a 读取 0-3字节
b 读取 4-11字节
c 读取 12-13字节

从上面看来,似乎没有什么问题,按着这个偏移量去读取指定字节数量的数据不就行了? 这我们就需要考虑内存读取的方式了。

内存的读取

专有名词

二维矩阵中的一个元素一般存储着8Bit

8个(Bank)相同位置(行列位置)的元素(小电容),一起组成在内存中连续的64Bit

图例分析
内存条.jpg

以上为内存条效果,内存条中每一个小黑块就是一个chip。此内存条有8chip

Chip内部构造图.jpg

以上为Chip的内部结构,及每次读取8字节的方式:从每个Bank的相同位置,各读取一个字节,然后拼接为8字节的数据。

从上图展示的内存的物理结构我们可以看出:

  1. 内存中最小的单位就是小电容,最小就是1个字节。所以操作系统管理的时候,最小的单位也是1个字节。

  2. 在内存中连续的64Bit,其实在内存的物理结构中并不连续,而是分散在同位置的8Bank上。

内存对齐的本质

内存硬件设计分为多个Chip,每个Chip内部维护这8Bank表,每个Bank表包含着行列矩阵的电容单元。

内存在进行IO的时候,一次操作取的就是64bit。所以内存对齐最底层的原因就是内存的IO64bit为单位进行操作。

所以在64CPU上,我们每次读取是一次8字节。那么就是:

0-7Byte     一次读取
8-15Byte    一次读取
16-23Byte   一次读取

那么回到我们上面遗留的问题

从上面看来,似乎没有什么问题,按着这个偏移量去读取指定字节数量的数据不就行了?

从这里我们可以发现,当我们访问一个变量的时候,最好的方式就是CPU一次能够读取出需要的数据。那么我们看上面假设的读取方式,能否一次读取需要的数据。

a 读取 0-3字节          0-7Byte 一次读取可成功
b 读取 4-11字节         0-7Byte 8-15Byte 需要两次读取才可以获取到 
c 读取 12-13字节        8-15Byte 一次读取可成功

从上面分析可以发现,我们读取b时需要内存工作两次才能正确读取。
这还是a0开始的时候,那么如果a不是从0开始,那我们就需要CPU操作内存更多次才能获取需要的信息。
在数据量比较大的情况下这无疑是一个巨大的消耗。以下为验证消耗的用例:

#pragma pack(1)     # 不使用内存对齐
typedef struct {
    short a;    // 2 Byte   出现一个不足8字节,后面但凡是超出8字节的全部都是一次内存读取不成功的
    long b;     // 8 字节
    int c;      // 4 Byte
    double d;   // 8 Byte
    int64_t e;
    long f;
    long g;
}AStruct;

#pragma pack(8)
typedef struct {
    short a;    // 2 Byte
    long b;     // 8 字节
    int c;      // 4 Byte
    double d;   // 8 Byte
    int64_t e;
    long f;
    long g;
}BStruct;

static int maxCount = 1000000000;

void testUnAlign() {
    AStruct a = {1,200000000,3,100.0f,1000000000,10012032,324324234};
    NSLog(@"%d-%ld-%d-%.2f-%d-%ld-size:%d",a.a,a.b,a.c,a.d,a.e,a.f,sizeof(a));
    CFAbsoluteTime start = CFAbsoluteTimeGetCurrent();
    for (int i = 0; i < maxCount; i++) {
        AStruct a = {1,200000000,3,100.0f,1000000000,10012032,324324234};
        int64_t sum = a.a + a.b + a.c + a.d + a.e + a.f + a.g;
    }
    CFAbsoluteTime end = CFAbsoluteTimeGetCurrent();
    NSLog(@"未对齐耗时:%f",end - start);
}

void testAlign() {
    
    BStruct a = {1,200000000,3,100.0f,1000000000,10012032,324324234};
    NSLog(@"%d-%ld-%d-%.2f-%d-%ld-size:%d",a.a,a.b,a.c,a.d,a.e,a.f,sizeof(a));
    CFAbsoluteTime start = CFAbsoluteTimeGetCurrent();
    for (int i = 0; i < maxCount; i++) {
        BStruct a = {1,200000000,3,100.0f,1000000000,10012032,324324234};
        int64_t sum = a.a + a.b + a.c + a.d + a.e + a.f + a.g;
    }
    CFAbsoluteTime end = CFAbsoluteTimeGetCurrent();
    NSLog(@"对齐耗时:%f",end - start);
}

// 测试效果
testUnAlign();
testAlign();

# TestMemoryAlign[4805:290147] 1-200000000-3-100.00-1000000000-10012032-size:46
# TestMemoryAlign[4805:290147] 未对齐耗时:15.066716
# TestMemoryAlign[4805:290147] 1-200000000-3-100.00-1000000000-10012032-size:56
# TestMemoryAlign[4805:290147] 对齐耗时:5.225200

经由测试发现,结构体内变量越多,遍历次数maxCount越多,未对齐查找操作耗时就越多。

由此可以确定,内存对齐对程序性能的影响是极大的。

内存对齐的作用

为什么要内存对齐

不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能再某些地址处取某些特定类型的数据,否则抛出硬件异常。

通过上测试用例可发现

经过内存对齐后,CPU的内存访问速度大大提升

内存对齐会消耗一部分存储空间


0x03 内存对齐规则

对齐规则

内存对齐的规则【参考】:

  1. 结构体变量的起始地址能够被其最宽的成员大小整除
  2. 结构体每个成员相对于起始地址的偏移能够被其自身大小整除,如果不能则在前一个成员变量之后补充字节
  3. 结构体总体大小能够被最宽的成员的大小整除,如果不能则在后面补充字节。

上面的规则其实并不完善,还有内存对齐系数我们是可以指定的,所以还需要和下面配合处理内存对齐。

// x为对齐系数,对齐系数应该为多少 1,2,4,8,16 ; 1为不进行内存对齐
#pragma pack(x)

对齐规则整理如下:

  1. 字节对齐
    对于结构体各个成员,第一个成员位于偏移为0的位置,长度为y字节,对齐系数为x
a.下一个偏移量为 min(x,y) 的正整数倍; 
b.下一个变量的偏移量/变量大小 等于正整数。
  1. 整体对齐
    所有对齐后,结构体(或共用体)本身也要进行对齐,假设结构体(或共用体)内部最大数据成员长度为maxLength
那么结构体(或联合)整体大小为:min(x,maxLength) 的正整数倍

栗子

// #pragma pack(8)  // 64位机默认是8
typedef struct {
    int a;      // 4 Byte               0-3Byte   nextOffset = min(8,4)*倍数  8:默认对齐系数 4:前变量长度
    double b;   // 8 Byte               7-15Byte  这个大小为8Byte,那么偏移量就要是8字节的倍数,同时还要是min(8,4)的倍数,那么偏移量为8即可,
    short c;    // 2 Byte               16-17Byte 大小2Byte
}AStruct;

// 整体计算变量存储使用18Byte,但是整体还要进行对齐,最大变量长度为8字节,要是8的正整数倍,最小就是24Byte,所以结果就是24Byte

// 打印
AStruct a = {1,2.0f,3};
NSLog(@"%d-%.2f-%d-size:%d",a.a,a.b,a.c,sizeof(a));
# TestMemoryAlign[4907:301999] 1-2.00-3-size:24

// 我们还可以查看下内存结构确认是否是我们分析的那样
(lldb) p/x &a
(AStruct *) $0 = 0x00007ffeefbff508
(lldb) x/4g 0x00007ffeefbff508
0x7ffeefbff508: 0x0000000000000001 0x4000000000000000   # 1占了4个字节,然后填充4个字节0,然后是8字节的b
0x7ffeefbff518: 0x0000000000000003 0x00007ffeefbff558       # 3 占了后面两个字节,填充了6个字节的0,再后面的8个字节就不属于AStruct了

0x04 内存对齐使用

C 语言结构体共用体优化

通过上述操作,我们使用了内存对齐,但是耗费内存空间的大小可以通过调整成员变量的顺序来控制。

iOS是否需要优化

iOS在分配内存时已经调用了对齐操作

size_t class_getInstanceSize(Class cls)
{
    if (!cls) return 0;
    return cls->alignedInstanceSize();  // 返回对齐后的内存地址
}

// May be unaligned depending on class's ivars.
uint32_t unalignedInstanceSize() const {    // 直接获取所有变量的内存大小
    ASSERT(isRealized());
    return data()->ro->instanceSize;
}

// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() const {
    return word_align(unalignedInstanceSize());
}
    size_t size = alignedInstanceSize() + extraBytes;
    // CF requires all objects be at least 16 bytes.
    if (size < 16) size = 16;   // 如果分配的内存大小小于16,那么就为16
    return size;
}

总结

内存对齐对与我们开发者来说,基本是透明的,了解内部结构有助于我们的理解,深究则与硬件的组成有关联,有助于我们对内存知识的进一步了解。

扩展

如果不强制对地址进行操作,仅仅只是简单用C定义一个结构体,编译和链接器会自动替开发者对齐内存的。

尽量帮你保证一个变量不跨列寻址。

其实在内存硬件层上,还有操作系统层。操作系统还管理了CPU的一级、二级、三级缓存。

实际中不一定每次IO都从内存出,如果你的数据局部性足够好,那么很有可能只需要少量的内存IO,大部分都是更为高效的高速缓存IO

但是高速缓存和内存一样,也是要考虑对齐的。

参考:
什么是内存对齐,原理你真的了解吗?

如何理解 struct 的内存对齐?

上一篇 下一篇

猜你喜欢

热点阅读