eigen: выражение или функция для установки/возврата полного столбца

#c #c 17 #eigen #eigen3

Вопрос:

У меня есть несколько примеров в моем коде, где у меня есть условие, основанное на коэффициентах массивов 1xN, и мне нужно задать целые столбцы массивов MxN в зависимости от этих условий. В моем случае N равно Eigen::Dynamic , а M колеблется от 2 до 4, но в каждом случае является константой времени компиляции.

Вот простая функция, иллюстрирующая, что я имею в виду, с a массивами 1xN и b являющимися массивами 1xN, которые формируют условие, c являющимися массивом 2xN с дополнительными данными и res являющимися выходным параметром, столбцы которого всегда задаются как единое целое:

 #include <iostream>
#include <Eigen/Dense>

using namespace Eigen;

template<Index nRows>
using ArrayNXd = Array<double, nRows, Dynamic>;

using Array1Xd = ArrayNXd<1>;
using Array2Xd = ArrayNXd<2>;
using Array3Xd = ArrayNXd<3>;

void asFunction(
    Array3Xdamp; res,
    const Array1Xdamp; a, const Array1Xdamp; b, const Array2Xdamp; c
){
    for (Index col{0}; col<a.cols();   col){
        if ( a[col] > b[col] )
            res.col(col) = Array3d{
                 a[col]   b[col],
                (a[col]   b[col]) * c(0, col),
                (a[col] - b[col]) * c(1, col)
            };
        else
            res.col(col) = Array3d{
                 a[col] - b[col],
                 a[col]   b[col],
                (a[col]   b[col]) * (a[col] - b[col])
            };
    }
}


int main(){
    Array1Xd a (3), b(3);
    Array2Xd c (2, 3);
    
    a << 1, 2, 3;
    b << 0, 1, 2;
    c <<
        0, 1, 2,
        1, 2, 3;

    Array3Xd res (3,3);
    
    asFunction(res, a, b, c);

    std::cout << "as function:n" << res << "n";

    return 0;
}
 

Функции, подобные этой, используются в разделе, посвященном производительности* моего кода, и я чувствую, что оставляю производительность на столе, потому что использование циклов с Eigen типами обычно не является оптимальным решением.

*да, я составил его профиль.

Я написал ту же функцию , что и a NullaryExpr , но это было немного медленнее. Я думаю, что это имеет смысл, учитывая дополнительные оценки условий и ветвления для каждой строки:

 #include <iostream>
#include <Eigen/Dense>

using namespace Eigen;

template<Index nRows>
using ArrayNXd = Array<double, nRows, Dynamic>;

using Array1Xd = ArrayNXd<1>;
using Array2Xd = ArrayNXd<2>;
using Array3Xd = ArrayNXd<3>;

class MyFunctor
{
public:
    using Scalar = double;

    static constexpr Index
        RowsAtCompileTime { 3 },
        MaxRowsAtCompileTime { 3 },
        ColsAtCompileTime { Dynamic },
        MaxColsAtCompileTime { Dynamic };

    using DenseType = Array<
        Scalar  ,    RowsAtCompileTime,    ColsAtCompileTime,
        ColMajor, MaxRowsAtCompileTime, MaxColsAtCompileTime
    >;

private:
    typename Array1Xd::Nested m_a;
    typename Array1Xd::Nested m_b;
    typename Array2Xd::Nested m_c;

public:
    MyFunctor(
        const Array1Xdamp; a,
        const Array1Xdamp; b,
        const Array2Xdamp; c
    ) : m_a {a}, m_b {b}, m_c{c}
    {}

    bool cond(Index col) const {
        return m_a[col] > m_b[col];
    }

    Scalar func1(Index col) const {
        return m_a[col]   m_b[col];
    }

    Scalar func2(Index col) const {
        return m_a[col] - m_b[col];
    }

    Scalar func3(Index row, Index col) const {
        switch(row){
            case 0: return func1(col);
            case 1: return func1(col) * m_c(0, col);
            case 2: return func2(col) * m_c(1, col);
            default: __builtin_unreachable();
        }
    }

    Scalar func4(Index row, Index col) const {
        switch (row){
            case 0: return func2(col);
            case 1: return func1(col);
            case 2: return func1(col) / func2(col);
            default: __builtin_unreachable();
        }
    }

    Scalar operator() (Index row, Index col) const {
        if ( cond(col) )
            return func3(row, col);
        else
            return func4(row, col);
    }
};

using MyReturnType = Eigen::CwiseNullaryOp<
    MyFunctor, typename MyFunctor::DenseType
>;


MyReturnType asFunctor(
    const Array1Xdamp; a,
    const Array1Xdamp; b,
    const Array2Xdamp; c
){
    using DenseType = typename MyFunctor::DenseType;
    return DenseType::NullaryExpr(
        3, a.cols(),
        MyFunctor(a, b, c)
    );
}


int main(){
    Array1Xd a (3), b(3);
    Array2Xd c (2, 3);
    
    a << 1, 2, 3;
    b << 0, 1, 2;
    c <<
        0, 1, 2,
        1, 2, 3;

    std::cout << "as functor:n" << asFunctor(a,b,c) << "n";

    return 0;
}
 

Мой вопрос: существует ли более эффективный способ реализации логики, аналогичной приведенной выше (оценить скалярное условие для каждого столбца матрицы, возвращаемые значения для всего столбца на основе условия) с использованием eigen библиотеки?

Примечание: использование выражения было бы несколько предпочтительнее, потому что мне не нужно беспокоиться о выделении памяти, внешних параметрах и т. Д., А код может быть написан с учетом скаляров, что делает его гораздо более понятным.

Редактировать: Примечание 2: Я тоже пытался использовать <Condition>.template replicate<nRows,1>().select(..., ...) , но это было медленнее и труднее читать.

Ответ №1:

Вы можете использовать метод выбора Eigen, но он работает только для скаляров, поэтому вам нужно пройти по одному измерению.

 const auto condition = a > b;
res.row(0) = condition.select(a   b /*true*/, a - b /*false*/);
res.row(1) = condition.select((a   b) * c.row(0), a   b);
res.row(2) = condition.select((a - b) * c.row(1), (a   b) * (a - b));
 

Обратите внимание, что вы, вероятно, будете быстрее, если перенесете все свои массивы. Затем итерация идет столбец за столбцом, которые векторизованы намного лучше, так как Eigen является основным столбцом.

Комментарии:

1. Спасибо, но, как я написал в «Примечании2», я уже пробовал select . Кроме того, транспонирование не является вариантом, потому что тогда компоненты переменных не будут смежными в памяти. И массивы 1xN все еще могут быть основными по столбцам.

2. Кстати, в то время select как работает с коэффициентами, вы можете использовать replicate его в своем условии для целых столбцов. Так , например res = condition.replicate(3,1).select(...,...) , как я написал в своей второй заметке. Таким образом, вы все равно можете позволить Эйгену выполнить цикл.

Ответ №2:

поэтому я посмотрел только на этот фрагмент кода

     for (Index col{0}; col<a.cols();   col){
        if ( a[col] > b[col] )
            res.col(col) = Array3d{
                 a[col]   b[col],
                (a[col]   b[col]) * c(0, col),
                (a[col] - b[col]) * c(1, col)
            };
        else
            res.col(col) = Array3d{
                 a[col] - b[col],
                 a[col]   b[col],
                (a[col]   b[col]) * (a[col] - b[col])
            };
    }
 

Я подозреваю, но не могу доказать, что эти a[col] и b[col] получают доступ каждый раз, когда вы им звоните. Возможно, вам захочется попробовать создать короткие временные интервалы для значений, которые вы используете повторно. Например:
поэтому я посмотрел только на этот фрагмент кода

     for (Index col{0}; col<a.cols();   col){
        auto acol=a[col];
        auto bcol=b[col];
        auto apb=acol bcol;
        auto amb=acol-bcol;
        if ( acol > bcol )
            res.col(col) = Array3d{
                 apb,
                (apb) * c(0, col),
                (amb) * c(1, col)
            };
        else
            res.col(col) = Array3d{
                 amb,
                 apb,
                (apb) * (amb)
            };
    }
 

и да, я знаю, что это не совсем то, чего ты хотел. может быть, это поможет тебе

Комментарии:

1. На самом деле это был всего лишь упрощенный пример для моей логики кода. В моем реальном производственном коде все упрощения, о которых я мог подумать, уже были протестированы. Это включает в себя повторно используемые термины.

2. я должен был догадаться, учитывая, что ты сказал, что приложил усилия. Последняя возможность, о которой я могу подумать — вы рассматривали возможность построения разреженных матриц ( Eigen::SparseMatrix<double> sparsesMat ) из каждого из ваших терминов, суммирования их, а затем использования конструктора плотной матрицы, который принимает разреженную матрицу (` denseMat = MatrixXd(sparseMat)`)?

3. Нет, я вообще еще не использовал разреженные матрицы, потому что до сих пор в моем коде не было очевидного варианта их использования. Не могли бы вы рассказать немного подробнее? Звучит интересно 🙂

4. не могу сказать много — некоторое время назад я копался в этом, но потом решил вместо этого использовать тензоры. короче говоря, вы можете построить разреженный C_ij как f(i,j). Я не помню синтаксиса, но думаю, что он есть в документах. Соответствующий бит заключается в том, что существует плотный матричный конструктор из разреженной матрицы. наивно я думаю, что, может быть, вы можете использовать термины «если» в разреженной среде и другие термины в плотной среде, а затем объединить их