Spec-Zone.ru › Eigen3

Структуры с элементами Eigen

Обзор

Если вы определяете структуру, содержащую члены векторизуемых типов Eigen фиксированного размера, вы должны убедиться, что вызов оператора new для неё выделяет буферы с правильным выравниванием. Если вы компилируете только в режиме [c++17] с достаточно новой версией компилятора (например, GCC >= 7, clang >= 5, MSVC >= 19.12), то компилятор позаботится обо всём, и вы можете прекратить чтение.

В противном случае, вам нужно переопределить его operator new так, чтобы он генерировал правильно выровненные указатели (например, 32-байтовые для Vector4d и AVX). К счастью, 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 самостоятельно позаботится о требовании выравнивания по 256 бит для Eigen::Vector4d, сделав два шага:

  • Eigen требует выравнивания по 256 бит для массива Eigen::Vector4d (из 4 чисел двойной точности). В [c++11] это делается с помощью ключевого слова alignas, или расширений компилятора для c++98/03.
  • Eigen переопределяет operator new для Eigen::Vector4d, чтобы она всегда возвращала выровненные по 256 бит указатели. (удалено в [c++17])

Таким образом, обычно вам не нужно беспокоиться ни о чём, Eigen обрабатывает выравнивание оператора new для вас...

...кроме одного случая. Когда у вас есть class Foo как выше, и вы динамически выделяете новый Foo как выше, то, поскольку Foo не имеет выровненных operator new, возвращённый указатель foo не обязательно выровнен по 256 бит.

Атрибут выравнивания члена v относится к началу класса Foo. Если указатель foo не был выровнен, то foo->v тоже не будет выровнен!

Решение состоит в том, чтобы class Foo имел выровненный operator new, как мы показали в предыдущем разделе.

Это объяснение также справедливо для платформ SSE/NEON/MSA/Altivec/VSX, которые требуют выравнивания по 16 байтам, и AVX512, которая требует выравнивания по 64 байтам для объектов фиксированного размера, кратных 64 байтам (например, Eigen::Matrix4d).

Нужно ли помещать все члены типов Eigen в начало моего класса?

Это не обязательно. Поскольку Eigen заботится о декларации необходимого выравнивания, все члены, которые его нуждаются, автоматически выравниваются относительно класса. Поэтому код вроде этого работает нормально:

class Foo
{
  double x;
  Eigen::Vector4d v;
public:
  EIGEN_MAKE_ALIGNED_OPERATOR_NEW
};

Тем не менее, как обычно, рекомендуется сортировать члены таким образом, чтобы выравнивание не тратило память. В приведённом выше примере с AVX компилятор должен будет зарезервировать 24 пустых байта между x и v.

А что насчёт матриц и векторов динамического размера?

Матрицы и векторы динамического размера, такие как Eigen::VectorXd, динамически выделяют свой собственный массив коэффициентов, поэтому они автоматически заботятся о требовании абсолютного выравнивания. Поэтому они не вызывают эту проблему. Обсуждаемая здесь проблема касается только матриц и векторов фиксированного размера с векторизацией.

Это ошибка в Eigen?

Нет, это не наша ошибка. Это скорее внутренняя проблема спецификации языка C++, которая была решена в C++17 с помощью функции, известной как динамическое выделение памяти для данных с повышенным выравниванием.

Что если я хочу сделать это условно (в зависимости от параметров шаблона)?

Для этой ситуации мы предлагаем макрос EIGEN_MAKE_ALIGNED_OPERATOR_NEW_IF(NeedsToAlign). Он сгенерирует выровненные операторы, такие как EIGEN_MAKE_ALIGNED_OPERATOR_NEW, если NeedsToAlign истинно. Он сгенерирует операторы с дефолтным выравниванием, если NeedsToAlign ложно. В [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.
Licensed under the MPL2 License.
https://eigen.tuxfamily.org/dox/group__TopicStructHavingEigenMembers.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API