你了解内存对齐吗?| 8月更文挑战

342 阅读4分钟

关于内存对齐那点事

这是我参与8月更文挑战的第2天,活动详情查看:8月更文挑战

引言:之前看redis sds的源码以及讲解时候,经常会看见内存对齐可以提高效率,但是具体怎么实现的并不知道,最近温习redis底层数据结构的时候就顺带把内存对齐也看了。

1.内存对齐的定义

维基百科中是这样定义内存对齐的:

A memory address a is said to be n-byte aligned when a is a multiple of n bytes (where n is a power of 2). In this context, a byte is the smallest unit of memory access, i.e. each memory address specifies a different byte. An n-byte aligned address would have a minimum of log2(n) least-significant zeros when expressed in binary.

具体意思是:当A是n字节的倍数(其中n是2的幂)时,存储地址A被称为n字节对齐。在此上下文中,字节是内存访问的最小单位,即每个内存地址指定一个不同的字节。当用二进制表示的时候,一个n字节对齐的地址至少有log2(n)个最低有效零

维基百科里面的话不太适合小白理解,但其实内存对齐简单来说就是通过牺牲空间来提高代码运行效率的一种方案

需要内存对齐几个方面

  • 基本类型对齐,内存地址对齐。不同架构CPU平台上基本类型的对齐系数可能会不同。
  • CPU内存之间的高速缓存行cacheline对齐,也就是伪共享问题

2.为什么需要内存对齐

如果不内存对齐的话,由于硬件的限制,会导致访存次数增加,影响了效率。因为尽管内存以字节为单位,但是大部分处理器不是按字节来存取内存的,它们一般会以双字节,4字节,8字节,16字节以及32字节为单位来存取内存,我们将上述这些内存存取单位称为内存存取粒度

假设现在的系统为32位只能从地址为4的倍数的内存进行读取数据。如果没有内存对齐,数据任意存放,那么假如一个int类型的数据是从地址2开始存储的(占用2,3,4,5)那么内存在寻址的时候需要读取两个字节块,并且需要剔除0,1,6,7四个地址,并把地址2,3,4,5合并成一起,降低了效率,提高了成本。于是就有了内存对齐。

具体参考如下:
img

img

3.内存对齐规则

每个特定平台上的编译器都有自己默认“对齐系数”(也叫对齐模数)。

有效对齐值是指给定值"对齐系数"和数据结构体中最长数据类型长度中较小的那个。有效对齐值也叫对齐单位

对齐规则:

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

例子:

// 64位系统
#include<stdio.h>
struct{
    int a,
    char b,
    double c,
}s1;
struct{
    int a,
    double c,
    char b,
}s2;
struct{
    char b,
    int a,
    double c,
}s3;

int main(){
    printf("%d\n",sizeof(s1)); // 16
    printf("%d\n",sizeof(s2)); // 24
    printf("%d\n",sizeof(s3));  // 16
}

根据上面两条规则可以算出这三个数据结构所占的字节数。

s1内存占用(8字节)s2内存占用(8字节)s3内存占用(8字节)
a 4字节 b 1字节(8字节)a 4字节 (8字节)b 1字节 a 4字节 (8字节)
c 8字节 (8字节)c 8字节 (8字节)c 1字节 (8字节)
b 1字节 (8字节)
总计:8+8=16总计:8+8+8=24总计:8+8=16

s1为例进行分析:

a的字节数为4<=8字节,按照4字节对齐,占用第0,1,2,3单元;

b的字节数为1<=8字节,按照1字节对齐,相对于首地址的偏移要为四的倍数,占用第四单元;

c的字节数为8<=8字节,按照8字节进行对齐,相对于结构体首地址的偏移要为8的倍数,占用8-15单元;

同理可知其他两个数据结构的字节占用。

通过该实例的分析,大家应该对内存对齐有了稍微的了解。

4. 32位和64位系统下的内存对齐

1.64位

#include<stdio.h>
struct A
{
    int   a; // 4字节
    char  b; // 1字节
    double c; // 8字节
    char  d; // 1字节
};

struct B
{
    char  a;
    double b;
    char  c;
};

int main()
{
    // 结果:int=4,char=1,double=8
    printf("int =%lu,char=%lu,double=%lu\n",sizeof(int),sizeof(char),sizeof(double));
    // 结果:structA=24 structB=24
    printf("structA=%lu  structB=%lu\n",sizeof(struct A),sizeof(struct B));
    return 0;  
}

根据第三节的规则容易算出:

structA: 4+1+3(对齐)+8+1+7(对齐) =24

这里写图片描述

structB: 1+7(对齐)+8+1+7(对齐) = 24

这里写图片描述

2.32位

#include<stdio.h>
struct A
{
    int   a; // 4
    char  b; // 1
    double c; // 8
    char  d; // 1
};

struct B
{
    char  a;
    double b;
    char  c;
};

int main()
{
    //结果:int=4 char=1 double=8
    printf("int =%u,char=%u,double=%u\n",sizeof(int),sizeof(char),sizeof(double));
    //结果:structA=20 structB=16
    printf("structA=%u  structB=%u\n",sizeof(struct A),sizeof(struct B));
    return 0;
}

32位和64位系统计算出的字节是不一样的原因是32位中对齐模数是4字节,而64位系统中对齐模型是8字节

structA:

这里写图片描述

structB:

这里写图片描述

结语

本文到此就结束了,如果觉得还行的话可以点个赞哦!