内存对齐问题

背景

内存对齐可能很多程序员接触不到,也许只在面试的偶尔会被问到过,但是也只是背背固定的公式,大概知道怎么计算,也能知道大致的原理,就是数据不对齐,取数次数要变多,但是只是理解到这种程度还不够,目前intel cpu不需要对齐也能访问,但是对于一些新的arm芯片,自研芯片等等当内存不对齐时候直接宕机给你看,自己在做hpc时,发现这个内存对齐问题还是一个比较严重的事情,这里好好的捋顺一下,避免自己以后遗忘。

规则

  • 结构体中大小为 size 的字段,他的结构内偏移 offset 需要是其min(default, size)的整数倍 //default是#pragma pack(default)
  • 结构体整体的大小是min(default,max(struct))的整数倍(这是一个总结的结论,实际原因是保证数组[1]的成员变量都能按着正确的对齐方式放入)

查看默认值

g++ -fdump-lang-class test.cc

例子1
  • 情况1:
    假如现在有一个32位的cpu,default为4,现在有一个数据在2,3,4,5四个位置(每个位置一个字节),那么需要两次读取,第一次就是读取0123到寄存器中,第二次就是读取4567到寄存器中,然后拼接出来2345这个数据。在这里插入图片描述
    所以当size正好等于default(4)的时候,假如这个偏移位置是4的时候,就可以避免分两次来提取数字。
  • 情况2:
    当size大于default的时候,就无法避免分两次(也许更多次)来提取数字,所以偏移位置是default的时候就ok了。
  • 情况3:
    当size小于default的时候,现在有一个数据在2,3四个位置(每个位置一个字节),那么需要一次读取,第一次就是读取0123到寄存器中,然后左移两个字节
    在这里插入图片描述
    这个问题发现,如果一个数据结构的size小于一个读取size的时候,理论上来说放在1,2和放在2,3没啥区别,这个就涉及到数据传输的问题,假如这个数据传输到一个CPU只能读取2个字节的CPU,这样在1,2这样的话就必须新的CPU就只能读取两次,而23还是一次,所以大家就定一个规则你的偏移位置放在你自己size的整数倍。

总结

综上可以看到,其实你的size如果小于default偏移位置就是你的整数倍,如果你的size大于default,其实无所谓分开取数了(因为你本来都超过一次取数的大小),所以偏移位置就是default的整数倍。提炼出来就是规则1。

例子2

下面这段代码可以看到,理论来说结构应该是6, 但是其实是8,所以结构体末尾padding出两个0,这就比较奇怪了?为什么?
其实是因为为了搞数组问题,Std num[10];问题来了,可以看到num[1]中的首地址是6,而6不是4的整数倍,但是如果padding两个位置的话,首地址就是8。

#include <cstddef>
#pragma pack(4)/*指定按4字节对齐*/
typedef struct Student
{
    int i;    
    char c1;  
    char c2;  
} Std;
std::cout<<sizeof(Std)<<std::endl;
std::cout<<offsetof(Std, i)<<std::endl;
std::cout<<offsetof(Std, c1)<<std::endl;
std::cout<<offsetof(Std, c2)<<std::endl;
#pragma pack () /*取消指定对齐,恢复缺省对齐*/
例子3

结构嵌套内存对齐问题:

class Inner
{
	char a;
	char b;
	char c;
}
class Outter
{
	char d;
	Inner e;
}
sizeof(Inner) == 3;
sizeof(Outter) == 4;

根据上面的两个原则,这里的默认值是4字节。Inner的内存是3毋容置疑,但是Outter就有点奇怪了?理论来说e的偏移应该是min(4,3)的整数倍,那么e的位置应该是3,4,5,其中0号位置放d, 1,2位置空着,接着就是整体大小是min(4,3)的整数倍,所以Outter的size应该是6的,现在为啥是4呢?
这个问题原因是首先偏移位置算错了,不能把这个Inner当成一个3字节的内存,应该是min(4, max(Inner))的整数倍,其次Outter的整体size也是min(4,max(Inner))的整数倍。

总结

综上可以看到,其实你struct的结构的大小就是需要去padding的。提炼出来就是规则2。此外要清楚内存对齐是编译器做的事情,编译器并不清楚你运行的机器到底是按什么方式取地址的,所以一般只是提供一个大部分机器都使用的值。

测试代码
#pragma pack(4)//pack里面可以设置为(1,2,4,8,16)
#include<stdio.h>
struct
{
    int i;    
    char c1;  
    char c2;  
}x1;

struct{
    char c1;  
    int i;    
    char c2;  
}x2;

struct{
    char c1;  
    char c2; 
    int i;    
}x3;
//如果想单独对某一个类型采用对齐方式,可以如下写法
 struct Student
{
   char c1;
   int c2;
   double c3;
}__attribute__((aligned(8)));

int main()
{
    printf("%d\n",sizeof(x1));  // 输出8
    printf("%d\n",sizeof(x2));  // 输出12
    printf("%d\n",sizeof(x3));  // 输出8
    return 0;
}

参考

链接1
链接2

题外话

aligned和pack的意思完全不相同,这个意思是test的起始位置要是16的倍数,如果是结构的话,size也要是16的倍数

class test
{
  int a;
  int b;
};__attribute__((aligned(16)));

align的使用场景是:1. 变量 2. 类型

  • 当aligned作用于变量时,其作用是告诉编译器为变量分配内存的时候,要分配在指定对齐的内存上,作用于变量之上不会改变变量的大小。例子:int test _ attribute_((aligned(16)));该变量a的内存起始地址为16的倍数。
  • 当aligned作用于类型时,其作用是告诉编译器该类型声明的所有变量都要分配在指定对齐的内存上。当该属性作用于结构体声明时可能会改变结构体的大小
alignof

利用alignof可以查看当下的数据类型的pack值是多少。
也可以用下面的代码看一下:

/// Structure alignment
template <typename T>
struct AlignBytes
{
    struct Pad
    {
        T       val;
        char    byte;
    };

    enum
    {
        ALIGN_BYTES = sizeof(Pad) - sizeof(T)
    };

    /// The "truly aligned" type
    typedef T Type;
};
alignas

利用alignas来指定对齐大小,

### demo_1
struct alignas(2) A
{
        int a;
        char b;
};
alignof(A)==4 //max(alignas, max(A)=int)
sizeof(A)==8 //min(int, alignof),所以必须是4的整数倍

### demo_2
struct alignas(2) A
{
        int a;
        double b;
};
alignof(A)==8 //max(alignas, max(A)=double)
sizeof(A)==16 //min(double, alignof)所以必须是8的整数倍
### demo_3
struct alignas(2) A
{
		double b;
        int a;  
};
alignof(A)==8 //max(alignas, max(A)=double)
sizeof(A)==16 //min(double, alignof)所以必须是8的整数倍
### demo_4
#pragma pack(4)
struct alignas(2) A
{
		double b;
        int a;  
};
#pragma pack(4)
alignof(A)==4 //不知道为啥
sizeof(A)==12 //min(double, alignof)所以必须是4的整数倍

### demo_5
#pragma pack(4)
struct alignas(16) A
{
		double b;
        int a;  
};
#pragma pack(16)
alignof(A)==16 //不知道为啥
sizeof(A)==16 //min(double, alignof)所以必须是16的整数倍

aligned_storage
/*
template< std::size_t Len, std::size_t Align = default-alignment >struct::type aligned_storage;
相当于一个内建的POD类型他的大小是Size他的对齐方式是Align 
*/
typename std::aligned_storage<sizeof(T), __alignof(T)>::type data[N];

新理解

其实对一个结构或者对象,之所以给一个对齐属性是因为:编译器可以根据这个值选用不用的访存指令

#pragma pack(push, 8)
struct  alignas(16) test
{
        int a[10];  
};
#pragma pack(pop)
int main()
{
    test v;

    std::cout << "alignof= " << alignof(test)<<std::endl;
    std::cout << "alignof= " << alignof(test::a)<<std::endl;
    std::cout << "sizeof= " << sizeof(v)<<std::endl; //sizeof一定是alignof的整数倍
    std::cout << &v<< std::endl;
    long long ptr = reinterpret_cast<uintptr_t>(&v);
    std::cout << ptr <<std::endl;
    std::cout << ptr % 16 <<std::endl; //首地址一定是alignof(test)的整数倍;
}
//cout
alignof= 16
alignof= 4
sizeof= 48
0x7ffc376af460
140721238242400
0

上面的代码,一旦test的属性被定位16后,那么即使a[N]的N再大,那么编译器也只会调用ldg.16的指令去读取这个结构。所以如果机器支持ldg.N的向量化处理,那么这里最好设置为N,这样可以减少指令次数,提高性能。

pragma pack VS alignas

#pragma pack(push, 1)
struct   alignas(2) test
{
        int a[10];  
};
#pragma pack(pop)

test v;
std::cout << "alignof= " << alignof(test::a)<<std::endl;
std::cout << "alignof= " << alignof(test)<<std::endl;
std::cout << "sizeof= " << sizeof(v)<<std::endl;
  1. 首先先按没有alignas(2)这个去算,根据上面的规则得到:
alignof= 1
alignof= 1
sizeof= 40
  1. 然后加上alignas(2),其实这时候已经不影响内存struct内存的变量的对齐方式了,只是改变test的对齐方式,大小取N= max(alignas, test_align),所以sizeof也会是N的整数倍
alignof= 1
alignof= 2
sizeof= 40
  1. 测试
#pragma pack(push, 1)
struct   alignas(16) test
{
        int a[10];  
};
#pragma pack(pop)
//cout
alignof= 1
alignof= 16
sizeof= 48

下面是一些可以魔改的demo,可以用来验证自己的想法是不是正确:

#include <iostream>

#pragma pack(push, 2)
struct   alignas(16) test
{
        char a;
        double b;  
};
#pragma pack(pop)

int main()
{
    test v;

    std::cout << "alignof= " << alignof(test::b)<<std::endl;
    std::cout << "alignof= " << alignof(test)<<std::endl;
    std::cout << "sizeof= " << sizeof(test)<<std::endl;
    std::cout << &v<< std::endl;
    long long ptr = reinterpret_cast<uintptr_t>(&v);
    long long ptr_b = reinterpret_cast<uintptr_t>(&(v.b));
    std::cout << ptr <<std::endl;
    std::cout << ptr_b <<std::endl;
    std::cout << ptr % alignof(test) <<std::endl;
}

利用这个可以测测cpu上的向量化操作,gcc -march=native, 参考代码

#include <immintrin.h>
#include <iostream>

int main() {

  __attribute__ ((aligned (32))) double input1[3] = {1, 1, 1};
  __attribute__ ((aligned (32))) double input2[3] = {1, 2, 3};
  __attribute__ ((aligned (32))) double result[5];
  
  double* pt = &input1[2];
  pt = pt+1;
  *pt= 1.38392e39;
  std::cout << "address of input1: " << reinterpret_cast<uintptr_t>(input1) << std::endl;
  std::cout << "address of input2: " << reinterpret_cast<uintptr_t>(input2) << std::endl;

  __m256d a = _mm256_load_pd(input1);
  __m256d b = _mm256_load_pd(input2);
  __m256d c = _mm256_add_pd(a, b);

  _mm256_store_pd(result, c);

  std::cout << result[0] << " " << result[1] << " " << result[2] << " " << result[3] <<" "<<result[5]<< std::endl;

  return 0;
}

新理解

编译器默认会给一个8字节的对齐需求,所以一个struct中的数据都是按照默认的8(可以pragma pack修改)去对齐,为了对齐这个值struct会扩大size。下面这个例子很容易就可以求出来size=8, size决定以后,那么Align这个对象实际地址应该放在哪里?实际会放在结构体中最大的元素的size整数倍,由于基础元素都是1,2,4,8,16等,只要保证了里面最大的那个数值对齐后,其他的元素肯定满足整数倍。

//defalut align=8
struct Align
{
        int a;
        char b;
        short c;
}

其次是pragma pack 和 align的区别,pragma pack作用于结构内的成员变量,类似于告诉编译器对面机器的取值规则;attribute ((aligned(n)))作用于结构体分配地址的对齐方式 和 结构体的大小,主要是人为告诉编译器这个值具体应该放在哪里,给了程序员一个自主权,由于这个值设定了,有时候还会导致size在尾部进一步补充扩张。这个所有的设置都是说在栈上的,因为这个是编译器可以确定的,对于堆上的对齐需要程序员自己去留意对齐问题了。cuda中的意思又不一样,可以参考这个https://stackoverflow.com/questions/73463957/how-align-works-in-cuda-c

#include <iostream>
#include <cstddef>


#pragma pack(1)
struct Align
{
        int a;
        char b;
        short c;
}__attribute__((aligned(2)));
#pragma pack()

int main()
{
	Align test[2];
	std::cout << &test[0] << std::endl;
	std::cout << &test[1] << std::endl;
	std::cout << "sizeof= " << sizeof(Align)<<std::endl;
	std::cout << "alignof= " << alignof(Align)<<std::endl;
	std::cout<<offsetof(Align, a)<<std::endl;
	std::cout<<offsetof(Align, b)<<std::endl;
	std::cout<<offsetof(Align, c)<<std::endl;
}
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
回答: 在设计结构体时,我们可以通过调整成员的顺序和使用#pragma pack指令来实现内存对齐和节省空间的目标。为了满足对齐要求,我们可以将占用空间小的成员尽量集中在一起,这样可以减少内存浪费。例如,可以调整结构体成员的顺序,将占用空间小的成员放在一起,以便在内存中占用连续的空间。另外,我们还可以使用#pragma pack指令来修改编译器的默认对齐数,以实现更灵活的内存对齐方式。通过设置合适的对齐数,我们可以在满足对齐要求的同时,尽量减少结构体的总大小。内存对齐的存在是为了提高处理器读写数据的效率。处理器通常以块为单位进行数据读写,如果结构体没有进行内存对齐,可能需要多次读写才能完成一个操作,从而降低了效率。因此,通过进行内存对齐,可以减少处理器的读写次数,提高数据访问的效率。 #### 引用[.reference_title] - *1* *2* [struct结构体的内存对齐](https://blog.csdn.net/chirrupy_hamal/article/details/102634695)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [关于struct内存对齐问题](https://blog.csdn.net/element137/article/details/69075284)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值