чего мне ожидать, когда я использую плотную матричную структуру данных Eigen?

Мне нужно использовать матричную структуру данных в моей программе, в то время как C++ имеет двумерные массивы, это очень низкий уровень, в то время как некоторые библиотеки, такие как Eigen, обеспечивают более высокий уровень матричной структуры данных. Но, на мой взгляд, независимо от того, насколько хорошо библиотека работает в некоторых высококвалифицированных операциях, таких как svd, высокая скорость основных операций, включая чтение (доступ), запись, суммирование, точка, должна быть предварительным требованием таких библиотек. . Поскольку в реальных приложениях такие базовые операции могут выполняться гораздо чаще, чем высококвалифицированные, то если библиотека на таких операциях тормозит, то это может оказаться бременем или даже узким местом системы.

Итак, я пишу несколько очень простых программ, используя как 2d-массив, так и плотную матрицу Eigen3 (MatrixXd), и сравниваю их производительность на 4 основных операциях. Оказывается, что в большинстве случаев 2d-массив выигрывает у Eigen3, что весьма разочаровывает. Я перечисляю некоторые из результатов моего теста ниже (код находится в конце приложения):

Матрица 10000X10000, команда компиляции: g++ -o test.o test.cpp -O0 -msse2

Эйген:

[!COST] инициализация: 6,8 сек.

[!COST] чтение: 14,85 сек.

[!COST] запись: 23,02 сек.

Сумма [!COST]: 3,28 сек.

[!COST] точка: 3,12 сек.

Цена за конверсию:

[!COST] инициализация: 1,81 сек.

[!COST] чтение: 2,4 сек.

[!COST] запись: 3,4 сек.

Сумма [!COST]: 0,63 сек.

[!COST] точка: 0,52 сек.

Матрица 10000X10000, команда компиляции: g++ -o test.o test.cpp -O3 -msse2

Эйген:

[!COST] инициализация: 2,44 сек.

[!COST] чтение: 2,16 сек.

[!COST] запись: 2,18 сек.

Сумма [!COST]: 0,26 сек.

[!COST] точка: 0,26 сек.

Цена за конверсию:

[!COST] инициализация: 1,71 сек.

[!COST] чтение: 2,06 сек.

[!COST] запись: 2,24 сек.

Сумма [!COST]: 0,15 сек.

[!COST] точка: 0,06 сек.

Тем не менее, у меня все еще есть некоторые сомнения по этому поводу, может быть, я не должен ожидать, что абстракция более высокого уровня матричной структуры будет работать так же быстро, как ее необработанная версия, если да, то чего мне ожидать, используя такую ​​​​библиотеку, как Eigen? Обратите внимание, что в моей программе есть некоторые высококвалифицированные операции, такие как SVD, а есть более простые операции, такие как доступ к матрице и запись матрицы.

Приложение, test.cpp:

#include <iostream>
#include <Eigen/Dense>
#include <ctime>
using Eigen::MatrixXf;

inline int cpp_testor_read(float **m, const int M, const int N)
{
    float randomTmp = 0;
    for (int i = 0; i < M; i ++)
        for (int j = 0; j < N; j ++)
        {
            randomTmp += m[i][j];
            randomTmp -= m[j][i];
        }
    return randomTmp;
}

inline int eigen_testor_read(MatrixXf m, const int M, const int N)
{
    float randomTmp = 0;
    for (int i = 0; i < M; i ++)
        for (int j = 0; j < N; j ++)
        {
            randomTmp += m(i, j);
            randomTmp -= m(j, i);
        }
    return randomTmp;
}

inline int cpp_testor_write(float **m, const int M, const int N)
{
    for (int i = 0; i < M; i ++)
        for (int j = 0; j < N; j ++)
        {
            m[i][j] += m[j][i];
            m[j][i] -= m[i][j];
        }
    return m[rand()%10000][rand()%10000];
}

inline int eigen_testor_write(MatrixXf m, const int M, const int N)
{
    for (int i = 0; i < M; i ++)
        for (int j = 0; j < N; j ++)
        {
            m(i, j) += m(j, i);
            m(j, i) -= m(i, j);
        }
    return m(rand()%10000, rand()%10000);
}

inline int cpp_testor_sum(float **m, const int M, const int N, float val)
{
    for (int i = 0; i < M; i ++)
        for (int j = 0; j < N; j ++)
        {
            m[i][i] += m[i][j];
        }
    return m[rand()%1000][rand()%1000];
}

inline int eigen_testor_sum(MatrixXf m, const int M, const int N, float val)
{
    m += m;
    return m(0, 0);
}

inline int cpp_testor_dot(float **m, const int M, const int N, float val)
{
    float randomTmp = 0;
    for (int i = 0; i < M; i ++)
        for (int j = 0; j < N; j ++)
        {
            m[i][j] *= val;
        }
    return m[rand()%1000][rand()%1000];
}

inline int eigen_testor_dot(MatrixXf m, const int M, const int N, float val)
{
    m *= val;
    return m(0, 0);
}

float** cpp_generator_mtarix(const int M, const int N)
{
    float **m = new float*[M];
    for (int i = 0; i < M; i ++)
        m[i] = new float[N];
    return m;
}

MatrixXf& eigen_generator_matrix(const int M, const int N)
{

    static MatrixXf m(M,N);
    return m;
}

int main()
{
    const int M = 10000;
    const int N = M;
    int antiopt = 0;
    srand(time(NULL));
    float val1 = rand()%10000 + 1;
    float val2 = rand()%10000 + 1;
    std::cout<< M << " " << N << std::endl;

    std::cout<<"Eigen:" << std::endl;
    size_t t = clock();
    //MatrixXf m = eigen_generator_matrix(M, N);
    MatrixXf m(M,N);
    for (int i = 0; i < M; i ++)
        for (int j = 0; j < N; j ++)
            m(i,j) = rand()%1000 + 1;
    t = clock() - t;
    std::cout<< "[!COST] init: " << t/float(CLOCKS_PER_SEC) << " sec." <<std::endl;

    t = clock();
    antiopt += eigen_testor_read(m,M,N);
    t = clock() - t;
    std::cout<< "[!COST] read: " << t/float(CLOCKS_PER_SEC) << " sec." <<std::endl;

    t = clock();
    antiopt += eigen_testor_write(m,M,N);
    t = clock() - t;
    std::cout<< "[!COST] write: " << t/float(CLOCKS_PER_SEC) << " sec." <<std::endl;

    t = clock();
    antiopt += eigen_testor_sum(m,M,N, val1);
    t = clock() - t;
    std::cout<< "[!COST] sum: " << t/float(CLOCKS_PER_SEC) << " sec." <<std::endl;

    t = clock();
    antiopt += eigen_testor_dot(m,M,N, val2);
    t = clock() - t;
    std::cout<< "[!COST] dot: " << t/float(CLOCKS_PER_SEC) << " sec." <<std::endl;

    std::cout<<"CPP:" << std::endl;
    t = clock();
    //float **mm = cpp_generator_mtarix(M, N);
    float **mm = new float*[M];
    for (int i = 0; i < M; i ++)
        mm[i] = new float[N];
    for (int i = 0; i < M; i ++)
        for (int j = 0; j < N; j ++)
            mm[i][j] = rand()%1000 + 1;
    t = clock() - t;
    std::cout<< "[!COST] init: " << t/float(CLOCKS_PER_SEC) << " sec." <<std::endl;

    t = clock();
    antiopt += cpp_testor_read(mm,M,N);
    t = clock() - t;
    std::cout<< "[!COST] read: " << t/float(CLOCKS_PER_SEC) << " sec." <<std::endl;

    t = clock();
    antiopt += cpp_testor_write(mm,M,N);
    t = clock() - t;
    std::cout<< "[!COST] write: " << t/float(CLOCKS_PER_SEC) << " sec." <<std::endl;

    t = clock();
    antiopt += cpp_testor_sum(mm,M,N, val1);
    t = clock() - t;
    std::cout<< "[!COST] sum: " << t/float(CLOCKS_PER_SEC) << " sec." <<std::endl;

    t = clock();
    antiopt += cpp_testor_dot(mm,M,N, val2);
    t = clock() - t;
    std::cout<< "[!COST] dot: " << t/float(CLOCKS_PER_SEC) << " sec." <<std::endl;

    std::cout<<antiopt<<std::endl;
}

person chentingpc    schedule 03.07.2013    source источник


Ответы (1)


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

Вместо этого вы должны передавать матрицы по ссылке, чтобы избежать накладных расходов на копирование и получить ту же семантику, что и версия массива. С этим изменением я получаю такие результаты, которые мне кажутся достаточно быстрыми:

10000 10000
Eigen:
[!COST] init: 3.5 sec.
[!COST] read: 2.98 sec.
[!COST] write: 3.03 sec.
[!COST] sum: 0.06 sec.
[!COST] dot: 0.07 sec.
CPP:
[!COST] init: 1.46 sec.
[!COST] read: 3.41 sec.
[!COST] write: 3.57 sec.
[!COST] sum: 0.14 sec.
[!COST] dot: 0.05 sec.

(Также обратите внимание, что сравнение с -O0 бессмысленно: вы явно говорите компилятору, чтобы он не ускорялся.)

person sth    schedule 03.07.2013
comment
Да, это как и мои результаты, которые я только что получил, когда я изменил все (хотя и встроенные) функция вызывает принять MatrixXf& вместо просто MatrixXf - person bobobobo; 03.07.2013
comment
Да! Спасибо, я не могу поверить, что проигнорировал это ... но кажется, что при последовательном доступе 2d-массив все еще в несколько раз быстрее, чем собственный, я добавляю как m[i][j], так и m[j][i] в мой тестовый код, но если использовать только m[i][j] в массиве 2d и m(j, i) в собственном массиве, массив 2d работает в 3 раза быстрее. Не уверен, почему - person chentingpc; 03.07.2013
comment
@chentingpc Из-за того, как компьютеры кэшируют память, итерация сначала по столбцам или строкам будет иметь разную производительность в зависимости от того, является ли ваша матрица основной ряд или столбец. - person David Brown; 03.07.2013
comment
@DavidBrown Я заметил, что поэтому я использовал m[i][j] в 2d-массиве и m(j,i) в собственном, я обнаружил основную причину того, что 2d-массив более быстр при последовательном доступе (что означает m[i ][j] вместо m[j][i] во внешнем и внутреннем цикле j): при использовании оптимизации O3 будет включена регистрация переименования (link), что ускоряет работу с двумерным массивом, но я не знаю, почему эта оптимизация O3 не работает с собственным объектом MatrixXf. - person chentingpc; 03.07.2013
comment
@chentingpc: Для меня последовательный доступ к m[i][j] и m(j,i) дает одинаковую производительность, не знаю, почему вы видите различия. - person sth; 03.07.2013