В настоящее время я пишу множество функций, которые принимают на вход блоки и выражения. Обычно мне гораздо проще работать со ссылками, поскольку они простые, легкие, а также легко гарантировать, что входящее выражение соответствует определенной форме (например, вектору).
В то же время я предполагаю, что должен быть какой-то недостаток, иначе подхода передачи аргументов с использованием шаблона MatrixBase<Derived>
не существовало бы. Тем не менее, я не смог найти ни одного сообщения, обсуждающего эту тему.
Поэтому я спрашиваю: каковы практические недостатки использования Refs вместо шаблонных параметров функции?
Обычно используются два типа Ref
: изменяемые Ref<Matrix<…>>
для выходных или входных параметров и неизменяемые Ref<const Matrix<…>>
для входных параметров. На самом деле они ведут себя немного по-другому.
Оба гарантируют, что входная матрица или вектор имеет внутренний шаг 1 во время компиляции, что означает, что элементы, по крайней мере, в одном столбце, соседствуют друг с другом. Это позволяет векторизацию.
Однако способы достижения этой цели различны. Изменяемая версия просто не сможет скомпилироваться, если указанный блок имеет другой шаг. Вместо этого неизменяемая версия, возможно, создаст временную копию. Это может иметь последствия для производительности и правильности.
Учти это:
double sum(const Eigen::Ref<const Eigen::VectorXd>& in)
{ return in.sum(); }
double foo()
{
Eigen::VectorXcd x = …;
return sum(x.real());
}
x.real()
создает представление комплексной матрицы. Поскольку комплексные значения хранятся в чередующемся формате реальных и мнимых компонентов, это представление имеет внутренний шаг, равный 2. Поэтому конструктор объекта Ref
выделит VectorXd
внутри и скопирует в него значения.
То же самое происходит здесь:
double foo()
{
Eigen::MatrixXd x = …;
return sum(x.row(0));
}
но этого не произойдет для x.col(0)
, поскольку Eigen по умолчанию использует формат по столбцам.
Если бы вместо этого вы написали ту же функцию с использованием MatrixBase<Derived>
, сама сумма была бы специализирована для фиксированного или переменного внутреннего шага выражений x.real()
или x.row()
. Это предотвратило бы векторизацию (ну, может быть, частичную векторизацию для x.real().sum()
все же можно было бы достичь), но также позволило бы избежать копирования.
В большинстве случаев потеря векторизации, вероятно, предпочтительнее создания копии, но это потребует тестирования.
Временная копия также будет создана, если входные данные представляют собой произвольное выражение. Например здесь:
double baz()
{
Eigen::VectorXd x = …;
return sum(x * 2.);
}
Здесь необходимо создать временный вектор со значением x * 2.
, поскольку преобразование нельзя передать в функцию sum
. Если бы sum
принял MatrixBase<Derived>
, код вместо этого был бы специализирован для объекта выражения и работал бы так же быстро и эффективно, как (x * 2.).sum()
.
В общем, когда вы используете Ref
, ни Eigen, ни компилятор не могут использовать информацию о конкретном типе или выражении, используемом в качестве параметра. Другим примером может быть передача Vector4d
в функцию sum
. Здесь теряется информация о том, что записей всегда 4, а значит цикл суммирования можно было развернуть.
В некоторых местах Eigen использует специализированные пути кода, если во время компиляции известно, что тип имеет фиксированный размер хотя бы в одном измерении, например, вычисление обратной небольшой матрицы или выполнение умножения матрицы с небольшой матрицей или вектором на одна сторона.
Когда вы используете простую матрицу или блок столбцов простой матрицы (например, matrix.middleCols(start, n)
в выражении), Эйген обычно может использовать информацию о том, что между одним столбцом нет пробела. Для простых скалярных операций вместо двойного значения цикл for(col = 0; col < cols; ++col) for(row = 0; row < rows; ++row)
, код будет оптимизирован в один цикл for(i = 0; i < rows * cols; ++i)
. Это может улучшить векторизацию, особенно для матриц с небольшим количеством строк и большим количеством столбцов.
Эта оптимизация невозможна с помощью Ref
, поскольку внешний шаг может быть больше, чем количество строк.
Незначительная проблема с производительностью заключается в том, что Ref
не гарантирует выравнивание начального адреса. Эйген предположит, что содержимое смещено. В основном это проблема, если вы компилируете без расширений AVX, поскольку SSE может складывать загрузку и сохранение памяти в арифметические операции только в том случае, если они гарантированно выровнены. Очень старое оборудование, предназначенное только для SSE, также было очень медленным для невыровненных инструкций загрузки/сохранения памяти, даже если они были выровнены во время выполнения.
Подробности см. в разделе Странное поведение выравнивания и SSE.
Что касается корректности, эти временные копии могут вызвать проблемы, если вы решите сохранить объект Ref
(или указатель на содержимое Ref
) дольше, чем сам вызов функции. Раньше мой код содержал такую функцию:
using ConstMapType = Eigen::Map<const Eigen::MatrixXd, Eigen::OuterStride<>>;
// Never do this!
ConstMapType block_to_map(const Eigen::Ref<const Eigen::MatrixXd>& block)
{
return ConstMapType(block.data(), block.rows(), block.cols(),
Eigen::OuterStride<>(block.outerStride()));
}
Если вы забудете, как это работает, и случайно вызовете это с каким-нибудь выражением, вызывающим временную копию, указатель data()
будет висеть в конце вызова функции.
К сожалению, вам придется оценивать влияние на производительность в каждом конкретном случае. Скорее всего, вы передадите простую матрицу/вектор или подблок? Будут ли они иметь динамический размер или можно будет включить фиксированный размер в Ref
, например. Ref<const Matrix4Xd>
? Тогда накладные расходы, вероятно, будут незначительными.
@Svalorzen Я не думаю, что это поддерживается. По крайней мере, я не вижу в коде ничего (например, макроса препроцессора), предназначенного для отключения копирования. Однако изменяемый класс Ref
не очень большой и его не очень сложно читать. Думаю, можно было бы скопировать его и создать свой собственный шрифт. gitlab.com/libeigen/eigen/-/blob/master/Eigen/src/Core/…
Спасибо за подробное объяснение. У меня вопрос: есть ли способ гарантировать, что const Ref не будет выполнять копии, а вместо этого не будет компилироваться, как и его неконстантный аналог (если, например, шаги не совпадают)?