Eigen::Map выравнивание необработанного буфера

При использовании библиотеки Eigen у меня есть шаблонный класс C++, содержащий необработанный буфер и Eigen::Map instance в качестве члена. В конструкторе класса я инициализирую карту следующим образом:

template<int size>
class TestEigenMapClass
{
public:
    TestEigenMapClass(): 
        
        vec_(vec_raw_,size) 
    {
        vec_.setZero();
    }
    Eigen::Map<Eigen::VectorXf> vec_;
    
private:
    int size_ = size;
    float vec_raw_[size];
};

Необработанный буфер выделяется системой. Должен ли я беспокоиться о выравнивании и производительности при объявлении или инициализации карты?

Он работает как есть, но меня интересуют различия в производительности, вызванные выравниванием, при компиляции этого кода на разных платформах. В документации для класса Eigen::Map просто говорится: «MapOptions указывает, является ли указатель выровненным или невыровненным. По умолчанию — невыровненным», но ничего больше.

Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
0
81
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Поскольку по умолчанию не выровнено, Eigen не будет предполагать никакого конкретного выравнивания. Так что код будет работать нормально. Стоимость этого зависит от вашей платформы:

  • На старом оборудовании SSE2 невыровненный доступ к памяти был очень медленным.
  • На оборудовании AVX или AVX2 они обычно работают быстро.
  • На железе AVX при компиляции только с инструкциями SSE2-4 для совместимости нельзя сворачивать операции с памятью в вычисления, что может незначительно влиять на производительность, особенно фронтенда (больше инструкций на то же количество микроопераций)
  • На оборудовании AVX-512, использующем полный размер вектора 64 байта, выровненный доступ снова становится более важным, поскольку он может получить одну строку кэша в одной инструкции, если правильно выровнен.

См., например, этот ответ для более полного обсуждения: Странное поведение выравнивания и SSE и здесь для AVX-512: Почему преобразование массива с использованием инструкций AVX-512 значительно медленнее при преобразовании его партиями по 8 по сравнению с 7 или 9?

Если вы хотите обеспечить правильное выравнивание, вы можете следовать руководству Eigen по этому. Однако вам необходимо внести некоторые коррективы, так как ваш массив нуждается в выравнивании и является последним элементом, в то время как выравнивание самого объекта Map не имеет значения.

Вот версия, которая должна работать на С++ 17 и выше:

template<int size>
class TestEigenMapClass
{
public:
    TestEigenMapClass(): 
        
        vec_(vec_raw_,size) 
    {
        vec_.setZero();
    }
    Eigen::VectorXf::AlignedMapType vec_;
    
private:
    int size_ = size;
    struct alignas(EIGEN_DEFAULT_ALIGN_BYTES) {
        float vec_raw_[size];
    };
};

Боковое примечание: не знаю, почему вы сохраняете размер как целочисленный член, когда и Map, и шаблон знают этот размер. Кроме того, я бы создал карту по запросу для экономии памяти следующим образом:

template<int size_>
class TestEigenMapClass
{
public:
    using map_type = Eigen::VectorXf::AlignedMapType;
    using const_map_type = Eigen::VectorXf::ConstAlignedMapType;

    TestEigenMapClass() = default;
    map_type vec() noexcept
    { return map_type(vec_raw_, size_); }

    const_map_type vec() const noexcept
    { return const_map_type(vec_raw_, size_); }

    int size() const noexcept
    { return size_; }
private:
    struct alignas(EIGEN_DEFAULT_ALIGN_BYTES) {
        float vec_raw_[size];
    };
};

Также обратите внимание, что вы можете просто поставить alignas на весь объект, если массив является первым элементом. Это также сэкономит место при заполнении байтов внутри объекта.

Я также предполагаю, что у вас есть веская причина не использовать тип Eigen фиксированного размера: Eigen::Matrix<float, size, 1>

вопросы и ответы

Проблема в том (я думаю), что создание карты будет вызывать выделение памяти (и некоторые дополнительные инструкции) каждый раз, когда доступ к массиву осуществляется через vec().

Нет. Map — это просто структура с указателем и размером. Его конструкция встроена. Это будет иметь нулевые накладные расходы. Рассмотрим этот пример кода:

void foo(TestEigenMapClass<16>& out,
         const TestEigenMapClass<16>& a,
         const TestEigenMapClass<16>& b)
{
    out.vec() = a.vec() + b.vec();
    out.vec() += b.vec() * 2.f;
}

Скомпилировано с помощью GCC-11.3, -std=c++20 -O2 -DNDEBUG получается вот такая сборка:

foo(TestEigenMapClass<16>&, TestEigenMapClass<16> const&, TestEigenMapClass<16>&):
        xor     eax, eax
.L2:
        movaps  xmm0, XMMWORD PTR [rdx+rax*4]
        addps   xmm0, XMMWORD PTR [rsi+rax*4]
        movaps  XMMWORD PTR [rdi+rax*4], xmm0
        add     rax, 4
        cmp     rax, 16
        jne     .L2
        xor     eax, eax
.L3:
        movaps  xmm0, XMMWORD PTR [rdx+rax*4]
        addps   xmm0, xmm0
        addps   xmm0, XMMWORD PTR [rdi+rax*4]
        movaps  XMMWORD PTR [rdi+rax*4], xmm0
        add     rax, 4
        cmp     rax, 16
        jne     .L3
        ret

Как видите, ноль накладных расходов. Просто загрузка, вычисление и сохранение векторов с плавающей запятой в двух циклах. Обратите внимание, что для того, чтобы это работало, вы должны скомпилировать его с помощью -DNDEBUG. В противном случае Eigen создаст утверждение и проверит выравнивание во время выполнения, когда вы используете выровненные карты. Это единственный случай, когда выровненный Map может иметь накладные расходы по сравнению с невыровненным Map. Но даже тогда это не должно иметь значения для производительности. Компиляторы и центральные процессоры умеют обходиться без нескольких простых проверок.

Во всяком случае, хранение карты имеет более высокие накладные расходы, поскольку вам нужно материализовать объект в памяти между вызовами функций и прочитать еще одну косвенность указателя (сначала прочитать Map через его ссылку, а затем поплавки через Map). Это также усложнит анализ алиасинга для компилятора.

Приведет ли EIGEN_DEFAULT_ALIGN_BYTES компилятор к автоматическому выбору наилучшего выравнивания?

Это макрос, установленный Эйгеном. Выбирается в зависимости от архитектуры. Если вы скомпилируете для SSE2-4, это будет 16, для AVX это 32. Не уверен, что AVX-512 поднимет это значение до 64. Это выравнивание для этой конкретной архитектуры.

Будьте осторожны с макетом структуры. Что-то вроде struct { int size; struct alignas(32) { float arr[]; }; }; потратит 28 байт на заполнение между int и float. Как обычно, поместите элемент с наибольшим выравниванием первым или иным образом позаботьтесь о том, чтобы не тратить место на отступы.

Возможно, я мог бы просто иметь шрифт фиксированного размера и каждый раз использовать head(). Меня интересует разница в производительности (это для высокопроизводительного прототипа в реальном времени).

head, tail, segment и т. д. в основном реализованы так же, как Map, поэтому у них такие же несуществующие накладные расходы. head() также по-прежнему несет информацию о времени компиляции о том, что вектор правильно выровнен. Если скомпилировать без -DNDEBUG, будет проверка диапазона. Но опять же, даже если вы оставите это активированным, обычно не о чем беспокоиться.

По возможности обязательно используйте параметр фиксированного шаблона вместо параметра размера среды выполнения для этих функций. vector.head<3>() более эффективен, чем vector.head(3).

Если вы планируете изменить размер вектора, вы также можете использовать параметр шаблона MaxRows, чтобы создать вектор, который никогда не выделяет память, но может изменять свой размер в определенном диапазоне:

template<int size>
using VariableVector = Eigen::Matrix<
      float, Eigen::Dynamic /*rows*/, 1 /*cols*/,
      Eigen::ColMajor | Eigen::AutoAlign,
      size /*max rows*/, 1 /*max cols*/>;

Спасибо за такой полный ответ, @ Homer512. Мне нравится ваше предложение создать карту на лету. Проблема в том (я думаю), что создание карты будет вызывать выделение памяти (и некоторые дополнительные инструкции) каждый раз, когда доступ к массиву осуществляется через vec(). Приведет ли EIGEN_DEFAULT_ALIGN_BYTES компилятор к автоматическому выбору наилучшего выравнивания?

Ernest 03.01.2023 09:11

Причина, по которой я не использую тип с фиксированным размером, заключается в том, что количество элементов, к которым нужно получить доступ, может меняться во времени (скажем, размер равен 16, а иногда осуществляется доступ только к 8 элементам, иногда к 12 и т. д.). Поэтому я подумал, что мог бы иметь Eigen::Map в качестве участника и менять карту на лету с помощью нового размещения. Но теперь, когда я думаю о том, что вы сказали в конце вашего ответа, я мог бы просто иметь тип фиксированного размера и каждый раз использовать head(). Меня интересует разница в производительности (это для высокопроизводительного прототипа в реальном времени).

Ernest 03.01.2023 09:11

@Ernest Эрнест, я расширил ответ, чтобы охватить ваши комментарии

Homer512 03.01.2023 17:20

это так полезно! Я бы хотел, чтобы документация включала такие типы объяснений и примеров.

Ernest 03.01.2023 21:10

@ Homer514 При расширении моих тестовых классов на другие случаи я столкнулся с проблемой: в другом классе у меня есть два таких необработанных буфера, скажем, vec1_raw_ и vec2_raw_, каждый из которых объявлен внутри struct alignas(EIGEN_DEFAULT_ALIGN_BYTES) { }; (по одному на каждый). Когда я пытаюсь присвоить одно другому через vec1() = vec2(), остальные переменные класса перепутались. Я делаю что-то незаконное?

Ernest 13.01.2023 06:02

@ Эрнест Нет, я так не думаю. Возможно, что-то не так с размером. Не могли бы вы опубликовать это в новом вопросе, чтобы не только я смотрел на код?

Homer512 13.01.2023 09:08

Я только что нашел проблему, действительно проблема размера. Виноват. И еще раз спасибо, вы очень помогли!

Ernest 13.01.2023 22:31

Другие вопросы по теме