夜妖娆莫言殇当使用X86 SSE,AVX指令集或ARM neon指令集对计算做加速时,会要求内存对齐,如果内存没有对齐,则会产生硬件异常中断。
如果内存不对齐,由于CPU从内存一次读取4个字节,而且必须内存对齐,这是由地址总线寻址机制决定的。如果数据没有对齐,读取int型数据需要读取2次,而如果内存对齐了,则只需要读取1次。由于内存读取相对CPU来说比较耗时,因此采用了空间换时间的策略,浪费一部分空间,从而提高读写效率。如果内存没有对齐则会产生异常中断。
内存对齐的好处就是加快内存读取效率,但它也不是完全没有缺点,它带来了2个副作用。
内存对齐会导致空间浪费,上面例子中原来只需要5个字节,由于内存对齐因此需要8个字节,编译器会在char和int之间添加3个字节的pad,从而保证内存对齐。因此变量的顺序可能会影响结构体的大小。例如
因此保持变量的良好申明顺序可以节省空间,但也会带来额外的问题,类初始化的时候是按照变量的顺序来初始化的,因此改变了变量的申明顺序也需要注意初始化的顺序是否有影响。
通过gcc编译选项-march=native可以使能X86 SSE,AVX指令集或ARM neon指令集,如果一个动态库使能了上述选项,而调用者没有使能,或者相反,都会导致错误,而gcc并不能查询当时动态库的编译选项,就会导致二进制不兼容的问题。
解决的办法很简单,就是将2者重新编译,要么都支持内存对齐,要么都不支持。
关于内存对齐,还是要回到内存的本质,程序中内存分为栈和堆,栈的内存是系统分配的,由系统申请和释放,堆是由用户主动申请的,由用户负责申请和释放。
栈上的内存是由编译器指定的,通过预编译选项可以告诉编译器需要对齐的字节数,进行对齐。也就是说对于栈上的内存,我们只需要通过关键字指定需要对齐的字节数,编译器会自动对齐。对于C++内置类型,char,int,float,double等不需要指定对齐的字节数,编译器默认会根据类型对齐。
但函数的参数是个例外,如果函数的参数需要内存对齐,不要通过传值的方式,而是通过引用或者指针的方式。
其实函数的参数可以认为是栈上的内存,为什么不能直接传值呢?实际上函数的传参可以理解为在传递参数之前,函数的参数自己复制了一份,然后开始调用函数。而函数的入口地址是固定的,这样如果函数的入口地址没有对齐,那么就会导致参数没有对齐,因此会引起异常。
栈上的内存通过关键字alignas和alignof对齐,而堆上的内存就相对比较麻烦,因为堆上的内存通过malloc返回,我们再看以下例子。
A是16字节对齐,因此申明变量由于是栈上分配内存,编译器会自动16字节对齐,如果是new出来的对象,不是由编译器分配,而是由new动态分配,因此没法保证16字节对齐。如果要对齐,只能重构BAR的new方法,采用aligned_alloc申请对齐的内存。
实际上c++ 17提供了aligned_alloc对齐的内存申请,因此如果是c++17,你可以不用考虑上述问题,但函数的参数还是需要注意,c++20引入了assume_aligned这样函数的参数也解决了。
也就是说编译器和语法越来越方便,可以让用户更少的考虑内存对齐的问题,从而变得更加高效。
eigen专门有一篇文章来讲述内存对齐Alignment issues,实际上我一开始遇到eigen内存对齐的问题时候也很困惑,为什么要加上宏EIGEN_MAKE_ALIGNED_OPERATOR_NEW,一开始我是从这些宏定义来入手,并没有理解底层原理,后来看了从Eigen向量化谈内存对齐这篇文章的分析,发现我之前思考问题的方式就错了。
内存对齐归根结底就是要考虑内存如何对齐,而不是考虑什么宏,为什么要添加这些宏,再加上并没有深入去查看这个宏的原理,导致理解起来有很多偏差,这里也告诉我们一个道理,遇到问题不理解的时候,一定要理解底层原理才能彻底理解清楚。
实际上这个宏定义就是用来重构new的,也就是自己实现了一个内存对齐的malloc和free,而c++17之后加入了标准,也就不需要在添加这个宏定义了。
为什么只有fixed-size的才有问题,而且是16字节对齐的才需要,Eigen::Vector2d是16字节对齐的,但Eigen::Vector3d不是,因此也没必要字节对齐。
还有MatrixXf也不需要指定字节对齐,因为它是动态数组,也就是说它的内存申请释放是eigen已经重构为对齐的malloc,所以不需要单独指定。
Eigen指针并不占用空间,因此包含eigen指针的结构体和类也不需要内存对齐。
|