[Eigen中文文档] 包含Eigen对象的结构体

简介: 如果定义的结构体包含固定大小的可向量化 Eigen 类型成员,则必须确保对其调用 operator new 来分配正确的对齐缓冲区。如果仅使用足够新的编译器(例如,GCC>=7、clang>=5、MSVC>=19.12)以 [c++17] 模式编译,那么编译器会自动处理所有事情,可以跳过本节。否则,必须重载它的 operator new 以便它生成正确对齐的指针(例如,Vector4d 和 AVX 的 32 字节对齐)。幸运的是,Eigen 为提供了一个宏 EIGEN_MAKE_ALIGNED_OPERATOR_NEW 来完成这项工作。

文档总目录

英文原文(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::Vector4doperator 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,编译器必须在 xv 之间保留 24 个空字节。

动态大小的矩阵和向量呢?

动态大小的矩阵和向量,例如 Eigen::VectorXd,会动态分配它们自己的元素数组,因此它们会自动处理要求绝对对齐的问题,所以他们没有这个问题。这里讨论的问题仅适用于固定大小的可向量化矩阵和向量。

这是 Eigen 中的Bug吗?

不,这不是 Eigen 中的Bug,它更像是 c++ 语言规范的固有问题,已在 c++17 中通过称为 过度对齐数据的动态内存分配 功能解决。

怎样有条件地执行此操作(取决于模板参数)?

对于这种情况,我们提供宏 EIGEN_MAKE_ALIGNED_OPERATOR_NEW_IF(NeedsToAlign)。如果 NeedsToAligntrue,它将生成对齐的运算符,如 EIGEN_MAKE_ALIGNED_OPERATOR_NEW。如果 NeedsToAlignfalse,它将生成具有默认对齐方式的运算符。在 [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 类在对齐问题上保持不变。缺点是无论如何都需要额外的堆内存空间分配。

相关文章
|
存储 编译器
[Eigen中文文档] 深入了解 Eigen - 类层次结构
本页面介绍了Eigen类层次结构中 Core 类的设计及其相互关系。一般用户可能不需要关注这些细节,但对于高级用户和Eigen开发人员可能会有用。
291 0
|
编译器 索引
[Eigen中文文档] 块操作
本文介绍了块操作。块是matrix或array的部分矩形元素。块表达式既可以用作右值也可以用作左值。与Eigen表达式一样,如果让编译器进行优化,则块操作的运行时间成本为零。
148 0
|
存储 算法 NoSQL
[Eigen中文文档] 存储顺序
矩阵和二维数组有两种不同的存储顺序:列优先和行优先。本节解释了这些存储顺序以及如何指定应该使用哪一种。
164 0
|
C++
[Eigen中文文档] 按值将Eigen对象传递给函数
对于 Eigen,这一点更为重要:按值传递固定大小的可向量化 Eigen 对象不仅效率低下,而且可能是非法的或使程序崩溃! 原因是这些 Eigen 对象具有对齐修饰符,在按值传递时会不遵守这些修饰符。
165 0
|
存储 C语言 C++
|
存储 编译器
[Eigen中文文档] 编写以特征类型为参数的函数(一)
Eigen使用表达式模板的方式导致每个表达式的类型可能都不同。如果将这样的表达式传递给一个需要Matrix类型参数的函数,则表达式将隐式地被评估为一个临时Matrix,然后再传递给函数。这意味着失去了表达式模板的好处。
144 0
|
存储 算法 NoSQL
[Eigen中文文档] 稀疏矩阵操作
在许多应用中(例如,有限元方法),通常要处理非常大的矩阵,其中只有少数系数不为零。在这种情况下,可以通过使用仅存储非零系数的特殊表示来减少内存消耗并提高性能。这样的矩阵称为稀疏矩阵。
463 0
|
存储 索引
[Eigen中文文档] 扩展/自定义Eigen(三)
本页面针对非常高级的用户,他们不害怕处理一些Eigen的内部细节。在大多数情况下,可以通过使用自定义一元或二元函数避免使用自定义表达式,而极其复杂的矩阵操作可以通过零元函数(nullary-expressions)来实现,如前一页所述。 本页面通过示例介绍了如何在Eigen中实现新的轻量级表达式类型。它由三个部分组成:表达式类型本身、包含有关表达式编译时信息的特性类和评估器类,用于将表达式评估为矩阵。
135 1
|
存储 NoSQL API
[Eigen中文文档] Matrix类
在Eigen中,所有矩阵和向量都是Matrix模板类的对象。向量只是行数或者列数为1的特殊矩阵。
399 1