Собственный массив ConditionType: эффективный способ широковещательной передачи вместо цикла

У меня есть критически важный для производительности фрагмент кода, где мне нужно проверить один массив на значения ниже порога, а затем условно установить значения двух других массивов. Мой код выглядит так:

#include <Eigen/Dense>

int main(){
    Eigen::ArrayXXd
        a (1, 100),
        b (2, 100),
        c (3, 100);
    
    a.setRandom();
    b.setRandom();
    c.setRandom();
    
    constexpr double minVal { 1e-8 };
    
    /* the code segment in question */
    /* option 1 */
    for ( int i=0; i<2; ++i ){
        b.row(i)   = (a < minVal).select( 0, c.row(i+1) / a );
        c.row(i+1) = (a < minVal).select( 0, c.row(i+1) );
    }
    /* option 2, which is slower */
    b = (a < minVal).replicate(2,1).select( 0, c.bottomRows(2) / a.replicate(2,1) );
    c.bottomRows(2) = (a < minVal).replicate(2,1).select( 0, c.bottomRows(2) );

    return 0;
}

Массив a, значения которого проверяются на достижение порога minVal, имеет одну строку и динамическое количество столбцов. Два других массива b и c имеют две и три строки соответственно и такое же количество столбцов, что и a.

Теперь я хотел бы реализовать вышеупомянутую логику более eigen способом, без этого цикла в варианте 1, потому что обычно у eigen есть хитрости в рукаве для повышения производительности, которые я никогда не могу надеяться сопоставить при написании сырых циклов. Однако единственным выходом, который я мог придумать, был вариант 2, который заметно медленнее, чем вариант 1.

Каким будет правильный и эффективный способ сделать это? Или петля уже мой лучший вариант?


person RL-S    schedule 12.08.2020    source источник


Ответы (1)


Вы можете попробовать следующее:

  • Определите типы массивов с фиксированным числом строк и динамическим числом столбцов, т.е. вы можете заменить Eigen :: ArrayXXd на Eigen :: Array ‹double, 1/2/3, Eigen: : Динамический ›.
  • Используйте версию блочных операций фиксированного размера (см. https://eigen.tuxfamily.org/dox/group__TutorialBlockOperations.html), то есть вы можете заменить bottomRows (N) на bottomRows ‹N› () и аналогичным образом реплицировать (2,1) с помощью replicate ‹2,1› ().

Я изменил типы массивов в вашем коде и включил третий вариант с возможными улучшениями, которые я упомянул:

#include <Eigen/Dense>

#include <iostream>
#include <chrono>

constexpr int numberOfTrials = 1000000;
constexpr double minVal{ 1e-8 };

typedef Eigen::Array<double, 1, Eigen::Dynamic> Array1Xd;
typedef Eigen::Array<double, 2, Eigen::Dynamic> Array2Xd;
typedef Eigen::Array<double, 3, Eigen::Dynamic> Array3Xd;

inline void option1(const Array1Xd& a, Array2Xd& b, Array3Xd& c)
{
    for (int i = 0; i < 2; ++i) {
        b.row(i) = (a < minVal).select(0, c.row(i + 1) / a);
        c.row(i + 1) = (a < minVal).select(0, c.row(i + 1));
    }
}

inline void option2(const Array1Xd& a, Array2Xd& b, Array3Xd& c)
{
    b = (a < minVal).replicate(2, 1).select(0, c.bottomRows(2) / a.replicate(2, 1));
    c.bottomRows(2) = (a < minVal).replicate(2, 1).select(0, c.bottomRows(2));
}

inline void option3(const Array1Xd& a, Array2Xd& b, Array3Xd& c)
{
    b = (a < minVal).replicate<2, 1>().select(0, c.bottomRows<2>() / a.replicate<2, 1>());
    c.bottomRows<2>() = (a < minVal).replicate<2, 1>().select(0, c.bottomRows<2>());
}

int main() {
    Array1Xd a(1, 100);
    Array2Xd b(2, 100);
    Array3Xd c(3, 100);

    a.setRandom();
    b.setRandom();
    c.setRandom();

    auto tpBegin1 = std::chrono::steady_clock::now();
    for (int i = 0; i < numberOfTrials; i++)
        option1(a, b, c);
    auto tpEnd1 = std::chrono::steady_clock::now();

    auto tpBegin2 = std::chrono::steady_clock::now();
    for (int i = 0; i < numberOfTrials; i++)
        option2(a, b, c);
    auto tpEnd2 = std::chrono::steady_clock::now();

    auto tpBegin3 = std::chrono::steady_clock::now();
    for (int i = 0; i < numberOfTrials; i++)
        option3(a, b, c);
    auto tpEnd3 = std::chrono::steady_clock::now();

    std::cout << "(Option 1) Average execution time: " << std::chrono::duration_cast<std::chrono::microseconds>(tpEnd1 - tpBegin1).count() / (long double)(numberOfTrials) << " us" << std::endl;
    std::cout << "(Option 2) Average execution time: " << std::chrono::duration_cast<std::chrono::microseconds>(tpEnd2 - tpBegin2).count() / (long double)(numberOfTrials) << " us" << std::endl;
    std::cout << "(Option 3) Average execution time: " << std::chrono::duration_cast<std::chrono::microseconds>(tpEnd3 - tpBegin3).count() / (long double)(numberOfTrials) << " us" << std::endl;

    return 0;
}

Среднее время выполнения, которое я получил, следующее (i7-9700K, msvc2019, оптимизация включена, NDEBUG):

(Option 1) Average execution time: 0.527717 us
(Option 2) Average execution time: 3.25618 us
(Option 3) Average execution time: 0.512029 us

И с включенным AVX2 + OpenMP:

(Option 1) Average execution time: 0.374309 us
(Option 2) Average execution time: 3.31356 us
(Option 3) Average execution time: 0.260551 us

Я не уверен, что это самый лучший способ Eigen, но надеюсь, что это поможет!

person puhu    schedule 13.08.2020
comment
Я не могу использовать фиксированные массивы строк, потому что в противном случае я не могу использовать для них контейнер. Но я обязательно попробую фиксированные размеры блоков и посмотрю, исправит ли это это! - person RL-S; 16.08.2020
comment
Я только заменил типы массивов на ArrayXXd и продолжил использовать операции фиксированного размера. Полученное время выполнения: (Option 1) 0.50138 us, (Option 2) 6.46162 us, (Option 3) 2.1247 us. Несмотря на то, что улучшения все еще есть, кажется, что матрицы фиксированного размера в основном отвечали за предыдущую производительность. Если вы можете поделиться, я хочу спросить: что вы имеете в виду, используя контейнер? Вы используете другую библиотеку и меняете типы массивов для разных операций? - person puhu; 16.08.2020
comment
ArrayXXds - это члены данных класса, которые затем сохраняются в контейнере (std :: array), который я перебираю в цикле. Поэтому мне пришлось бы создать шаблон класса, что приведет к разным типам для разного количества строк. Тогда я больше не могу хранить их в контейнере (кроме std :: variant, но это тоже накладные расходы). Однако их не так уж и много, поэтому я оставлю это как вариант. Но, учитывая, что вариант 1 работает в любом случае, я сначала пойду по этому пути. Большое спасибо за ваши усилия! - person RL-S; 17.08.2020
comment
До сих пор я думал, что изменение только одного параметра размера на фиксированный размер не повлияет на производительность, потому что, если другой является динамическим, массив все равно должен быть размещен в куче. . Есть идеи, почему это может иметь такое значение? - person RL-S; 17.08.2020
comment
Для тех, кто пытается это дома: при использовании auto для создания собственного выражения, а затем использовании шаблонной функции-члена вы можете столкнуться с действительно странной error: expected unqualified-id before numeric constant. Затем вам нужно добавить template перед вызовом функции, например. (myEigenXpr * otherEigenXpr).template replicate<2,1>() Кредиты этому потоку: stackoverflow.com/questions/4942703/ - person RL-S; 17.08.2020
comment
option3 - принятый ответ. - person RL-S; 06.07.2021