【Eigen】Eigen内存对齐超详细讲解
只有固定大小向量化Eigen类型(fixed-size vectorizable Eigen types)才有可能需要用户额外进行内存对齐的操作,如Vector3f、MatrixXd等,都不需要用户操心。STL容器使用固定大小向量化Eigen类型对象成员自定义类含有固定大小向量化Eigen类型对象成员基本思想就是:对于基本数据类型和自定义类型,我们需要用预编译指令来保证栈内存的对齐,用重写oper
Eigen
是一个非常常用的矩阵运算库,由于使用了SSE
指令集进行向量化加速,因此它的矩阵运算能力还是很厉害的,在SLAM等领域是一个不可或缺的的工具。然而,有时候在vector容器或者class类中使用Eigen时,可能会出现一些奇奇怪怪的错误。
究其原因,可能是Eigen对象没有进行内存对齐,从而导致程序崩溃。
本文着重于Eigen什么情况下需要内存对齐?用户如何实现内存对齐的功能。而如果想要一步步看其演变过程,有一篇文章写的很好:从Eigen向量化谈内存对齐。
为什么要内存对齐
Eigen矩阵运算快的主要原因之一就是向量化运算。
所谓向量化运算,就是利用SSE、AVX等SIMD(Single Instruction Multiple Data)指令集,实现一条指令对多个操作数的运算,从而提高代码的吞吐量,实现加速效果。SSE是一个系列,包括从最初的SSE到最新的SSE4.2,支持同时操作16 bytes的数据,即4个float或者2个double。AVX也是一个系列,它是SSE的升级版,支持同时操作32 bytes的数据,即8个float或者4个double。
但向量化运算是有前提的,那就是内存对齐。SSE的操作数,必须16 bytes对齐,而AVX的操作数,必须32 bytes对齐。也就是说,如果我们有4个float数,必须把它们放在连续的且首地址为16的倍数的内存空间中,才能调用SSE的指令进行运算。
因此,对于Eigen对象来说,一旦需要使用SSE指令集进行向量化运算,那必然要求它从内存地址16 byte整数倍的地方开始。如果不满足这个条件,在程序的运行过程中,一旦开始使用SSE指令集,就会产生错误,甚至崩溃。
Eigen对象都需要内存对齐么?
Eigen类型一般可以分为两类:固定大小、非固定大小。
能够向量化运算的固定大小的Eigen类,也被称为固定大小向量化Eigen类型
(fixed-size vectorizable Eigen types)。这要求Eigen对象:具有固定大小并且该大小是16个字节的倍数(即16字节对齐的)。
例如:Eigen::Vector4d是16字节的倍数,但Eigen::Vector3d不是,它是8字节的倍数。下面把一些常见的Eigen类型对齐大小做了打印:
std::cout << alignof(Eigen::Vector2d) << std::endl; // 16
std::cout << alignof(Eigen::Vector3d) << std::endl; // 8
std::cout << alignof(Eigen::Vector4d) << std::endl; // 16
std::cout << alignof(Eigen::Vector2f) << std::endl; // 4
std::cout << alignof(Eigen::Vector3f) << std::endl; // 4
std::cout << alignof(Eigen::Vector4f) << std::endl; // 16
std::cout << alignof(Eigen::Matrix2d) << std::endl; // 16
std::cout << alignof(Eigen::Matrix3d) << std::endl; // 8
std::cout << alignof(Eigen::Matrix2f) << std::endl; // 16
std::cout << alignof(Eigen::Matrix3f) << std::endl; // 4
std::cout << alignof(Eigen::Matrix4d) << std::endl; // 16
std::cout << alignof(Eigen::Matrix4f) << std::endl; // 16
std::cout << alignof(Eigen::Affine3d) << std::endl; // 16
std::cout << alignof(Eigen::Affine3f) << std::endl; // 16
std::cout << alignof(Eigen::Quaterniond) << std::endl; // 16
std::cout << alignof(Eigen::Quaternionf) << std::endl; // 16
如果一个Eigen对象,它不是固定大小向量化Eigen类型,那么它就不会进行向量化操作,更不用说会有什么内存对齐的问题了。
非固定大小的Eigen对象,例如MatrixXf。动态大小的Eigen对象的内存由它自己管理和释放,也就是说它的内存申请和释放是Eigen已经重构为对齐的malloc,因此不需要用户额外指定字节对齐。
这里看来只有某些固定大小向量化Eigen类型才需要用户额外指定内存对齐,这也正是我们需要关注的。
栈上Eigen
对于局部变量(栈内存),它们的内存地址是在编译期确定的,也就是由编译器决定。所以通过预编译指令来实现栈空间的对齐对齐。不同编译器的预编译指令是不一样的,比如gcc的语法为__attribute__((aligned(16)))
,MSVC的语法为__declspec(align(16))
。当然,c++ 11提供了关键字alignas
来告诉编译器按照指定字节进行对齐,使用起来更方便。例如:
// sse_t类型的每个对象将对齐到16字节边界
struct alignas(16) sse_t
{
float sse_data[4];
};
// 数组cacheline将对齐到128字节边界
alignas(128) char cacheline[128];
同理,栈上Eigen也可以通过alignas来进行内存对齐的,具体的实现在Eigen/src/Core/DenseStorage.h中,固定大小Eigen对象中的数据结构是plain_array类型,通过模板来设置不同的内存对齐大小。
template <typename T, int Size, int MatrixOrArrayOptions,
int Alignment = (MatrixOrArrayOptions&DontAlign) ? 0
: compute_default_alignment<T,Size>::value >
struct plain_array
{
T array[Size];
// ...
};
template <typename T, int Size, int MatrixOrArrayOptions>
struct plain_array<T, Size, MatrixOrArrayOptions, 16>
{
EIGEN_ALIGN_TO_BOUNDARY(8) T array[Size];
// ...
};
template <typename T, int Size, int MatrixOrArrayOptions>
struct plain_array<T, Size, MatrixOrArrayOptions, 16>
{
EIGEN_ALIGN_TO_BOUNDARY(16) T array[Size];
// ...
};
其中,EIGEN_ALIGN_TO_BOUNDARY宏,就是调用了alignas来指定8字节对齐。其定义为:
#define EIGEN_ALIGN_TO_BOUNDARY(n) alignas(n)
可以看到plain_array类型在定义的时候,就已经会根据它是否需要内存对齐进行alignas对齐了。因此,栈上空间不需要用户额外进行内存对齐操作。
堆上Eigen
栈内存,它是由编译器在编译时确定,因此预编译指令会生效。但用堆内存,它的内存地址是在运行时确定。C++的运行时库并不会关心预编译指令声明的对齐方式,因此关键字alignas对于堆上Eigen没有作用。
STL容器
如果STL容器中的元素是Eigen数据结构时,其内存空间也是定义在堆上的。那么如何保证其的内存对齐呢?
由于c++11之前,STL并没有提供内存对齐的allocator方式,而Eigen自己的类型通过重构new和delete方法来实现了对齐的aligned_allocator,因此只需要在定义容器的时候指定Eigen实现的aligned_allocator即可。
例如,用vector容器存储Eigen::Matrix4f类型或用map存储Eigen::Vector4f数据类型时,不过不做任何处理:
std::vector<Eigen::Matrix4d>
std::map<int, Eigen::Vector4f>
这么使用的话,编译能通过,当运行时会报段错误。
这个时候,如果使用Eigen自己定义的内存分配器,则可避免这个问题(需要include头文件):
#include <Eigen/StdVector>
std::vector<Eigen::Vector4f, Eigen::aligned_allocator<Eigen::Vector4f> >
std::map<int, Eigen::Vector4f, std::less<int>,
Eigen::aligned_allocator<std::pair<const int, Eigen::Vector4f> > >
注意:map的第三个参数std::less只是默认值,但是我们必须包含它,因为我们要指定第四个参数,即分配器类型。
其实,上述的这种才是标准的定义容器方法。STL容器的内存申请默认是std::allocator,并没有内存对齐,因此使用Eigen类型的STL容器的时候必须指定Eigen::aligned_allocator用于内存对齐。
当然,还有一个方法,但可能不是那么常用,使用宏EIGEN_DEFINE_STL_VECTOR_SPECIALIZATION:
#include <Eigen/StdVector>
EIGEN_DEFINE_STL_VECTOR_SPECIALIZATION(Eigen::Matrix2d)
std::vector<Eigen::Vector2d>
需要注意的是:必须在所有该Eigen对象出现前使用这个宏;并且该宏只专用于vector,如果是map,则不能如此使用。
类成员
如果用户自己创建的类型中包含Eigen类型,那么在进行new、delete操作时,也会在堆上空间进行对象创建。此时,则需要用户自己重写operator new和operator delete方法来保证内存的对齐。
当然,为了简化用户的操作,Eigen提供了宏EIGEN_MAKE_ALIGNED_OPERATOR_NEW来帮助用户实现堆上申请的内存对齐(需要在public权限下)。例如:
class Foo {
public:
EIGEN_MAKE_ALIGNED_OPERATOR_NEW
private:
Eigen::Vector2d v;
};
Foo *foo = new Foo();
此时,就会对自定义类型的new、delete方法进行重写了。通过源码,可以看到在Eigen/src/Core/util/Memory.h中通过宏重写了new和delete:
#define EIGEN_MAKE_ALIGNED_OPERATOR_NEW_IF(NeedsToAlign) \
EIGEN_DEVICE_FUNC \
void *operator new(std::size_t size) { \
return Eigen::internal::conditional_aligned_malloc<NeedsToAlign>(size); \
} \
EIGEN_DEVICE_FUNC \
void *operator new[](std::size_t size) { \
return Eigen::internal::conditional_aligned_malloc<NeedsToAlign>(size); \
} \
EIGEN_DEVICE_FUNC \
void operator delete(void * ptr) EIGEN_NO_THROW { Eigen::internal::conditional_aligned_free<NeedsToAlign>(ptr); } \
EIGEN_DEVICE_FUNC \
// ...
当然,还有其他的方案可以避免程序出错。例如,直接禁用内存对齐:
class Foo {
Eigen::Matrix<double, 2, 1, Eigen::DontAlign> v;
};
它的作用是在使用v时禁用矢量化。但如果Foo的函数多次使用它,那么仍然可以通过将其复制到对齐的临时向量来重新启用矢量化:
void Foo::bar() {
Eigen::Vector2d av(v);
v = av;
}
按值传递Eigen对象
在C++中,按值传递对象几乎总是一个非常糟糕的做法,因为这意味着无用的副本,最好通过引用传递它们。
使用Eigen时,这甚至更重要:按值传递固定大小向量化Eigen类型,不仅效率低下,而且可能是非法的,或者会使程序崩溃!原因是这些Eigen对象具有对齐修饰符,当按值传递它们时,这些修饰符aren’t respected。
因此,例如,像这样的函数:
void my_function(Eigen::Vector2d v);
需要重写如下,通过引用传递v:
void my_function(const Eigen::Vector2d& v);
总结
只有固定大小向量化Eigen类型(fixed-size vectorizable Eigen types)才有可能需要用户额外进行内存对齐的操作,如Vector3f、MatrixXd等,都不需要用户操心。
对于固定大小向量化Eigen类型,在下面两种情况需要用户进行额外内存对齐:
- STL容器使用固定大小向量化Eigen类型对象成员
- 自定义类含有固定大小向量化Eigen类型对象成员
基本思想就是:对于基本数据类型和自定义类型,我们需要用预编译指令来保证栈内存的对齐,用重写operator new的方式保证堆内存对齐。对于嵌套的自定义类型,申请栈内存时会自动保证其内部数据类型的对齐,而申请堆内存时仍然需要重写operator new。
相关阅读
更多推荐
所有评论(0)