内存对齐

一、内存对齐

1.字长

计算机字长指的是CPU一次(一个时钟周期)能处理的最大长度,一般来说有如下属性

计算机字长 = 寄存器大小 = 数据总线宽度 = 地址长度 = 机器位数

计算机按次从内存读出数据,字长就是计算机每次从内存读出的数据长度,以字节为单位。

2.内存对齐的好处

  • 假设字长为4B,如果数据对齐,那么在内存中的状态就是

    image-20220827214958042

    CPU一次就能将所有数据读出,而如果没有进行字节对齐,计算机需要两个时钟周期才能读出所有数据

    image-20220827215016735
  • 如果未对齐,取数据时需要多次访存,假设某些情况下,可能需要多次访存,比如64位CPU,每次最多可获取8B,现在有一个8B长的变量,如果没有对齐,可能前6B存在前一个地址,后2B存在另一个地址,两次才能取出,如果对齐,那就只需要访存一次。

    img

  • 可能会破坏访存的原子性,常见的就是long long并发时的错误

    img

  • 某些ARM CPU不支持未对齐的内存访问(我没碰到过)

  • 可能会造成性能问题

    • 在 ARM v6/7 上未对齐的访问通常需要许多额外的周期才能完成
    • 在现代的 x86 处理器上,未对其的内存访问没有明显的性能损失。在这篇对 Intel SandyBridge 架构(酷睿 2xxx 系列,奔腾 G6xx 系列)的测试文章里提到there is noperformance penalty for reading or writing misaligned memory operands
    • 不仅如此,在这篇文章的测试中,在一些 workload 下,未对齐的内存访问甚至比对齐的访问更快!

二、结构体对齐

1.对齐规则

  1. 数据类型的对齐:结构体中的成员变量按照自身的大小进行对齐。例如,int 按照 4 字节对齐,double 按照 8 字节对齐,char 按照 1 字节对齐。
  2. 对齐边界:结构体的起始地址必须是其最大成员的对齐边界的倍数。换句话说,结构体的大小必须是最大成员大小的整数倍
  3. 填充字节:为了满足对齐要求,编译器可能会插入填充字节,使得成员变量正确对齐。

以下面几个结构体为例

案例1

struct test{    
 char   a;    
 int    b;    
 short  c;    
}test1;  

这个结构体中,最长的变量长度为4B

image-20230725114647825

案例2

struct test{    
 char   A;    
 short  C;   
 int    B;    
}test2;  
image-20230725114758537

为什么需要填充?

填充才能保证内存对齐

为什么结构体的大小必须是最大成员大小的整数倍?

这篇回答中,找到了一个可能的原因,以这个结构体为例

struct st{
    int32_t a;
    int8_t  b;
};
 
struct st arr[N];

如果不对齐为整数倍,那么占用5B,如果对齐,那就是8B,但是如果不填充,当对数组访问时,arr[1].a就会有4B放在arr[0].b中,导致cross line,从而对齐失败,

所以第二条实际上还是为了内存对齐,如果没有第二条,还是存在未对齐的隐患。

四.保证内存对齐的简单算法

#include <stddef.h>
#include <stdlib.h>

int posix_memalign(void **memptr, size_t alignment, size_t size) {
    if (alignment % sizeof(void*) != 0 || (alignment & (alignment - 1)) != 0) {
        // 对齐要求必须是void*大小的倍数,并且是2的幂次
        return EINVAL; // 参数错误
    }

    void* ptr = malloc(size + alignment - 1);
    if (ptr == NULL) {
        return ENOMEM; // 内存分配失败
    }

    uintptr_t addr = (uintptr_t)ptr;
    uintptr_t aligned_addr = (addr + alignment - 1) & ~(alignment - 1);

    // 为了保存原始指针地址,需要将指针地址存储在指针指针(memptr)指向的位置
    *(void**)memptr = (void*)aligned_addr;

    return 0; // 成功
}
  1. 为什么是void*的倍数

    void*的大小一般是机器字长,所以含义就是首先必须和机器字长对齐

  2. 如何检测出是2的幂次

    2的幂次数有个特性,那就是只有最高位是1,剩下的都是0,比如8,

    而减去1后,又变成最高位是0,剩下都是1

    8:   1000
    8-1: 0111
    

    此时求与,如果结果是0,那就是有2的幂次,否则就不是,

    这是只有2的幂次具有的性质

  3. 如何保证对齐到alignment

    (addr + alignment - 1) & ~(alignment - 1);
    

三、附录

3.1 Golang中的内存对齐

make创建的Slice是会自动内存对齐的,比如int32会4B对齐,int64会8B对齐,简单写一个程序验证

func TestMakeAlignment(t *testing.T) {
	for sz := 1; sz < 10000; sz++ {
		buf32 := make([]int32, sz)
		address := uint64(uintptr(unsafe.Pointer(&buf32[0])))
		if address%(uint64(4)) != 0 {
			t.Fatalf("0x%x--%v is not aligned\n", address, 4)
		}

		buf64 := make([]int64, sz)
		address = uint64(uintptr(unsafe.Pointer(&buf64[0])))
		if address%(uint64(8)) != 0 {
			t.Fatalf("0x%x--%v is not aligned\n", address, 8)
		}
	}
}

3.2 C语言查看计算机位数

printf("%d", sizeof(size_t));
printf("%d", sizeof(void *));

3.3 C语言结构体内存对齐

// 默认字节对齐
// 当使用 __attribute__ ((__packed__)) 定义时,不进行对齐
struct
// struct __attribute__ ((__packed__))
test {
    uint8_t a;
    uint64_t b;
    uint16_t c;
};

void checkIfAligned(void *addr){
    int addrNumber = (int)(addr);
    printf("Address: 0x%x\n", addr);
    if(addrNumber % sizeof(void*) == 0){
        printf("true\n");
    } else {
        printf("false\n");
    }
}

int main() {
    struct test t;
    printf("Alignment: %lu\n", sizeof(void *));
    checkIfAligned(&t.a);
    checkIfAligned(&t.b);
    checkIfAligned(&t.c);
}

结果

// 对齐
Alignment: 8
Address: 0x6af8f118
true
Address: 0x6af8f120
true
Address: 0x6af8f128
true

// 不对齐
Alignment: 8
Address: 0x6d787125
false
Address: 0x6d787126
false
Address: 0x6d78712e
false
posted @ 2023-07-25 14:00  INnoVation-V2  阅读(140)  评论(0编辑  收藏  举报