При использовании библиотеки 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 указывает, является ли указатель выровненным или невыровненным. По умолчанию — невыровненным», но ничего больше.
Поскольку по умолчанию не выровнено, Eigen не будет предполагать никакого конкретного выравнивания. Так что код будет работать нормально. Стоимость этого зависит от вашей платформы:
См., например, этот ответ для более полного обсуждения: Странное поведение выравнивания и 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*/>;
Причина, по которой я не использую тип с фиксированным размером, заключается в том, что количество элементов, к которым нужно получить доступ, может меняться во времени (скажем, размер равен 16, а иногда осуществляется доступ только к 8 элементам, иногда к 12 и т. д.). Поэтому я подумал, что мог бы иметь Eigen::Map
в качестве участника и менять карту на лету с помощью нового размещения. Но теперь, когда я думаю о том, что вы сказали в конце вашего ответа, я мог бы просто иметь тип фиксированного размера и каждый раз использовать head()
. Меня интересует разница в производительности (это для высокопроизводительного прототипа в реальном времени).
@Ernest Эрнест, я расширил ответ, чтобы охватить ваши комментарии
это так полезно! Я бы хотел, чтобы документация включала такие типы объяснений и примеров.
@ Homer514 При расширении моих тестовых классов на другие случаи я столкнулся с проблемой: в другом классе у меня есть два таких необработанных буфера, скажем, vec1_raw_
и vec2_raw_
, каждый из которых объявлен внутри struct alignas(EIGEN_DEFAULT_ALIGN_BYTES) { };
(по одному на каждый). Когда я пытаюсь присвоить одно другому через vec1() = vec2()
, остальные переменные класса перепутались. Я делаю что-то незаконное?
@ Эрнест Нет, я так не думаю. Возможно, что-то не так с размером. Не могли бы вы опубликовать это в новом вопросе, чтобы не только я смотрел на код?
Я только что нашел проблему, действительно проблема размера. Виноват. И еще раз спасибо, вы очень помогли!
Спасибо за такой полный ответ, @ Homer512. Мне нравится ваше предложение создать карту на лету. Проблема в том (я думаю), что создание карты будет вызывать выделение памяти (и некоторые дополнительные инструкции) каждый раз, когда доступ к массиву осуществляется через
vec()
. Приведет лиEIGEN_DEFAULT_ALIGN_BYTES
компилятор к автоматическому выбору наилучшего выравнивания?