英文原文(Structures Having Eigen Members)
摘要
如果定义的结构体包含固定大小的可向量化 Eigen 类型成员,则必须确保对其调用 operator new
来分配正确的对齐缓冲区。如果仅使用足够新的编译器(例如,GCC>=7、clang>=5、MSVC>=19.12)以 [c++17] 模式编译,那么编译器会自动处理所有事情,可以跳过本节。
否则,必须重载它的 operator new
以便它生成正确对齐的指针(例如,Vector4d 和 AVX 的 32 字节对齐)。幸运的是,Eigen 为提供了一个宏 EIGEN_MAKE_ALIGNED_OPERATOR_NEW
来完成这项工作。
需要修改什么样的代码?
需要更改的代码类型如下:
class Foo
{
...
Eigen::Vector2d v;
...
};
...
Foo *foo = new Foo;
即如果有一个类,该类的成员是一个固定大小的可向量化 Eigen 对象,则需要动态创建该类的对象。
这样的代码应该如何修改?
只需要将 EIGEN_MAKE_ALIGNED_OPERATOR_NEW
宏放在类的公共部分,如下所示:
class Foo
{
...
Eigen::Vector4d v;
...
public:
EIGEN_MAKE_ALIGNED_OPERATOR_NEW
};
...
Foo *foo = new Foo;
这个宏使 new Foo 返回一个对齐的指针。在 [c++17] 中,这个宏是空的,因为编译器会自动处理所有事情。
如果此方法过于麻烦,另请参阅其他解决方案,见下文。
为什么需要这样修改?
假设有如下代码:
class Foo
{
...
Eigen::Vector4d v;
...
};
...
Foo *foo = new Foo;
一个 Eigen::Vector4d
由 4 个双精度数组成,即 256 位。这正好是 AVX 寄存器的大小,这使得可以使用 AVX 对该向量进行各种操作。但是 AVX 指令(至少是 Eigen 使用的指令速度很快)需要 256 位对齐,否则会出现段错误。
出于这个原因,Eigen 通过以下两点要求 Eigen::Vector4d
进行 256 位对齐:
- Eigen 通过
alignas
关键字要求Eigen::Vector4d
的数组(4 个双精度数)进行 256 位对齐。 - Eigen 重载了
Eigen::Vector4d
的operator new
,因此它将始终返回 256 位对齐的指针。 (在 [c++17] 中删除)
通常情况下,Eigen 会处理 operator new
的对齐,但当有一个像上面那样的 Foo
类,并且像上面那样动态分配一个新的 Foo
时,由于 Foo
没有对齐的 operator new
,返回的指针 foo
不一定是 256 位对齐的。
成员 v
的对齐属性依赖于类 Foo
的属性,如果 foo
指针没有对齐,那么 foo->v
也不会对齐!通常是让类 Foo
有一个对齐的 operator new
,正如我们在上一节中展示的那样。
此解释也适用于需要 16 字节对齐的 SSE/NEON/MSA/Altivec/VSX
对象,以及需要 64 字节对齐的 AVX512
固定大小对象(例如,Eigen::Matrix4d)。
是否应该把 Eigen 类型的所有成员放在类的开头?
这不是必需的。由于 Eigen 会自动处理对齐,所以像这样的代码是正常的:
class Foo
{
double x;
Eigen::Vector4d v;
public:
EIGEN_MAKE_ALIGNED_OPERATOR_NEW
};
也就是说,像往常一样,建议对成员进行排序,以便对齐不会浪费内存。在上面的示例中,对于 AVX,编译器必须在 x
和 v
之间保留 24 个空字节。
动态大小的矩阵和向量呢?
动态大小的矩阵和向量,例如 Eigen::VectorXd
,会动态分配它们自己的元素数组,因此它们会自动处理要求绝对对齐的问题,所以他们没有这个问题。这里讨论的问题仅适用于固定大小的可向量化矩阵和向量。
这是 Eigen 中的Bug吗?
不,这不是 Eigen 中的Bug,它更像是 c++ 语言规范的固有问题,已在 c++17 中通过称为 过度对齐数据的动态内存分配 功能解决。
怎样有条件地执行此操作(取决于模板参数)?
对于这种情况,我们提供宏 EIGEN_MAKE_ALIGNED_OPERATOR_NEW_IF(NeedsToAlign)
。如果 NeedsToAlign
为 true
,它将生成对齐的运算符,如 EIGEN_MAKE_ALIGNED_OPERATOR_NEW
。如果 NeedsToAlign
为 false
,它将生成具有默认对齐方式的运算符。在 [c++17] 中,这个宏是空的。
示例如下:
template<int n> class Foo
{
typedef Eigen::Matrix<float,n,1> Vector;
enum {
NeedsToAlign = (sizeof(Vector)%16)==0 };
...
Vector v;
...
public:
EIGEN_MAKE_ALIGNED_OPERATOR_NEW_IF(NeedsToAlign)
};
...
Foo<4> *foo4 = new Foo<4>; // foo4 is guaranteed to be 128bit-aligned
Foo<3> *foo3 = new Foo<3>; // foo3 has only the system default alignment guarantee
其他解决方案
如果随处放置 EIGEN_MAKE_ALIGNED_OPERATOR_NEW
宏过于麻烦,至少还有两个其他解决方案。
禁用对齐
第一个是禁用固定大小成员的对齐要求:
class Foo
{
...
Eigen::Matrix<double,4,1,Eigen::DontAlign> v;
...
};
这里的 v
与对齐的 Eigen::Vector4d
完全兼容。但这样会使对 v
的加载/存储效率更低(通常略有减少,但这取决于硬件)。
私有结构体
第二个是将固定大小的对象存储到一个私有结构体中,该结构体将在主对象构造时动态分配:
struct Foo_d
{
EIGEN_MAKE_ALIGNED_OPERATOR_NEW
Vector4d v;
...
};
struct Foo {
Foo() {
init_d(); }
~Foo() {
delete d; }
void bar()
{
// use d->v instead of v
...
}
private:
void init_d() {
d = new Foo_d; }
Foo_d* d;
};
这里的明显优势是 Foo
类在对齐问题上保持不变。缺点是无论如何都需要额外的堆内存空间分配。