Почему MATLAB/Octave стирает пол с помощью С++ в проблемах с собственными значениями?

Я надеюсь, что ответ на вопрос в названии состоит в том, что я делаю что-то глупое!

Вот проблема. Я хочу вычислить все собственные значения и собственные векторы вещественной симметричной матрицы. Я реализовал код в MATLAB (фактически, я запускаю его с помощью Octave) и С++, используя Научную библиотеку GNU. Я предоставляю свой полный код ниже для обеих реализаций.

Насколько я понимаю, GSL поставляется с собственной реализацией BLAS API (в дальнейшем я называю это GSLCBLAS) и использовать эту библиотеку, которую я компилирую, используя:

g++ -O3 -lgsl -lgslcblas

GSL предлагает здесь использовать альтернативную библиотеку BLAS, такую ​​как самооптимизирующийся ATLAS, для повышения производительности. Я запускаю Ubuntu 12.04 и установил пакеты ATLAS из репозитория Ubuntu. В этом случае я компилирую, используя:

g++ -O3 -lgsl -lcblas -latlas -lm

Для всех трех случаев я выполнял эксперименты со случайно генерируемыми матрицами размером от 100 до 1000 с шагом 100. Для каждого размера я выполняю 10 собственных наборов с разными матрицами и усредняю ​​время. Результатом является следующее:

Plot of Results

Разница в производительности нелепо. Для матрицы размером 1000, Octave выполняет разложение в секунду; GSLCBLAS и ATLAS занимают около 25 секунд.

Я подозреваю, что я могу неправильно использовать библиотеку ATLAS. Любые объяснения приветствуются; спасибо заранее.

Некоторые примечания к коду:

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

  • В Octave строка triu(A) + triu(A, 1)' обеспечивает симметричную матрицу.

  • Если вы хотите скомпилировать код на С++ с вашей собственной машиной Linux, вам также нужно добавить флаг -lrt из-за функции clock_gettime.

  • К сожалению, я не думаю, что clock_gettime выходит на другие платформы. Попробуйте изменить его на gettimeofday.

Октавный код

K = 10;

fileID = fopen('octave_out.txt','w');

for N = 100:100:1000
    AverageTime = 0.0;

    for k = 1:K
        A = randn(N, N);
        A = triu(A) + triu(A, 1)';
        tic;
        eig(A);
        AverageTime = AverageTime + toc/K;
    end

    disp([num2str(N), " ", num2str(AverageTime), "\n"]);
    fprintf(fileID, '%d %f\n', N, AverageTime);
end

fclose(fileID);

Код С++

#include <iostream>
#include <fstream>
#include <time.h>

#include <gsl/gsl_rng.h>
#include <gsl/gsl_randist.h>
#include <gsl/gsl_eigen.h>
#include <gsl/gsl_vector.h>
#include <gsl/gsl_matrix.h>

int main()
{
    const int K = 10;

    gsl_rng * RandomNumberGenerator = gsl_rng_alloc(gsl_rng_default);
    gsl_rng_set(RandomNumberGenerator, 0);

    std::ofstream OutputFile("atlas.txt", std::ios::trunc);

    for (int N = 100; N <= 1000; N += 100)
    {
        gsl_matrix* A = gsl_matrix_alloc(N, N);
        gsl_eigen_symmv_workspace* EigendecompositionWorkspace = gsl_eigen_symmv_alloc(N);
        gsl_vector* Eigenvalues = gsl_vector_alloc(N);
        gsl_matrix* Eigenvectors = gsl_matrix_alloc(N, N);

        double AverageTime = 0.0;
        for (int k = 0; k < K; k++)
        {   
            for (int i = 0; i < N; i++)
            {
                for (int j = 0; j < N; j++)
                {
                    gsl_matrix_set(A, i, j, gsl_ran_gaussian(RandomNumberGenerator, 1.0));
                }
            }

            timespec start, end;
            clock_gettime(CLOCK_MONOTONIC_RAW, &start);

            gsl_eigen_symmv(A, Eigenvalues, Eigenvectors, EigendecompositionWorkspace);

            clock_gettime(CLOCK_MONOTONIC_RAW, &end);
            double TimeElapsed = (double) ((1e9*end.tv_sec + end.tv_nsec) - (1e9*start.tv_sec + start.tv_nsec))/1.0e9;
            AverageTime += TimeElapsed/K;
            std::cout << "N = " << N << ", k = " << k << ", Time = " << TimeElapsed << std::endl;
        }
        OutputFile << N << " " << AverageTime << std::endl;

        gsl_matrix_free(A);
        gsl_eigen_symmv_free(EigendecompositionWorkspace);
        gsl_vector_free(Eigenvalues);
        gsl_matrix_free(Eigenvectors);
    }

    return 0;
}

Ответы

Ответ 1

Я не согласен с предыдущим сообщением. Это не проблема с потоками, это проблема с алгоритмом. Причина, по которой matlab, R и октава стирают пол с помощью библиотек С++, объясняется тем, что их библиотеки на С++ используют более сложные и лучшие алгоритмы. Если вы читаете октаву, вы можете узнать, что они делают [1]:

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

Решение собственных задач/собственных векторов нетривиально. На самом деле его одна из немногих вещей "Numericical Recipes in C" рекомендует вам не реализовывать себя. (P461). GSL часто медленный, и это был мой первоначальный ответ. ALGLIB также медленный для своей стандартной реализации (я получаю около 12 секунд!):

#include <iostream>
#include <iomanip>
#include <ctime>

#include <linalg.h>

using std::cout;
using std::setw;
using std::endl;

const int VERBOSE = false;

int main(int argc, char** argv)
{

    int size = 0;
    if(argc != 2) {
        cout << "Please provide a size of input" << endl;
        return -1;
    } else {
        size = atoi(argv[1]);
        cout << "Array Size: " << size << endl;
    }

    alglib::real_2d_array mat;
    alglib::hqrndstate state;
    alglib::hqrndrandomize(state);
    mat.setlength(size, size);
    for(int rr = 0 ; rr < mat.rows(); rr++) {
        for(int cc = 0 ; cc < mat.cols(); cc++) {
            mat[rr][cc] = mat[cc][rr] = alglib::hqrndnormal(state);
        }
    }

    if(VERBOSE) {
        cout << "Matrix: " << endl;
        for(int rr = 0 ; rr < mat.rows(); rr++) {
            for(int cc = 0 ; cc < mat.cols(); cc++) {
                cout << setw(10) << mat[rr][cc];
            }
            cout << endl;
        }
        cout << endl;
    }

    alglib::real_1d_array d;
    alglib::real_2d_array z;
    auto t = clock();
    alglib::smatrixevd(mat, mat.rows(), 1, 0, d, z);
    t = clock() - t;

    cout << (double)t/CLOCKS_PER_SEC << "s" << endl;

    if(VERBOSE) {
        for(int cc = 0 ; cc < mat.cols(); cc++) {
            cout << "lambda: " << d[cc] << endl;
            cout << "V: ";
            for(int rr = 0 ; rr < mat.rows(); rr++) {
                cout << setw(10) << z[rr][cc];
            }
            cout << endl;
        }
    }
}

Если вам действительно нужна быстрая библиотека, возможно, вам нужно сделать настоящую охоту.

[1] http://www.gnu.org/software/octave/doc/interpreter/Basic-Matrix-Functions.html

Ответ 2

Я также столкнулся с проблемой. Реальная причина в том, что eig() в matlab не вычисляет собственные векторы, но код версии C выше. Разница в затраченном времени может быть больше одного порядка величины, как показано на рисунке ниже. Поэтому сравнение нечестно.

В Matlab, в зависимости от возвращаемого значения, фактическая функция называется другой. Чтобы заставить вычислять собственные векторы, следует использовать [V, D] = eig (A) (см. Коды ниже).

Фактическое время для вычисления задачи на собственные значения в значительной степени зависит от свойств матрицы и желаемых результатов, таких как

  • Реальный или сложный
  • Эрмитово/Симметрично или нет
  • Плотный или разреженный
  • Только собственные значения, собственные векторы, максимальное собственное значение и т.д.
  • Последовательный или параллельный

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

Кроме того, Matlab и Mathematica использовали распараллеливание. Это еще больше расширяет разрыв, который вы видите в несколько раз, в зависимости от машины. Разумно сказать, что вычисление собственных значений и собственных векторов общего комплекса 1000x1000 составляет около секунды и десять секунд без распараллеливания.

Compare Matlab and C Сравнение Matlab и C. "+ vec" означает, что коды включали вычисления собственных векторов. CPU% - это очень грубое наблюдение за потреблением процессора при N = 1000, которое ограничено сверху на 800%, хотя они должны полностью использовать все 8 ядер. Разрыв между Matlab и C меньше 8 раз.

Compare different matrix type in Mathematica Сравните различные типы матриц в Mathematica. Алгоритмы автоматически выбираются программой.

Matlab (с вычислением собственных векторов)

K = 10;

fileID = fopen('octave_out.txt','w');

for N = 100:100:1000
    AverageTime = 0.0;

    for k = 1:K
        A = randn(N, N);
        A = triu(A) + triu(A, 1)';
        tic;
        [V,D] = eig(A);
        AverageTime = AverageTime + toc/K;
    end

    disp([num2str(N), ' ', num2str(AverageTime), '\n']);
    fprintf(fileID, '%d %f\n', N, AverageTime);
end

fclose(fileID);

С++ (без вычисления собственных векторов)

#include <iostream>
#include <fstream>
#include <time.h>

#include <gsl/gsl_rng.h>
#include <gsl/gsl_randist.h>
#include <gsl/gsl_eigen.h>
#include <gsl/gsl_vector.h>
#include <gsl/gsl_matrix.h>

int main()
{
    const int K = 10;

    gsl_rng * RandomNumberGenerator = gsl_rng_alloc(gsl_rng_default);
    gsl_rng_set(RandomNumberGenerator, 0);

    std::ofstream OutputFile("atlas.txt", std::ios::trunc);

    for (int N = 100; N <= 1000; N += 100)
    {
        gsl_matrix* A = gsl_matrix_alloc(N, N);
        gsl_eigen_symm_workspace* EigendecompositionWorkspace = gsl_eigen_symm_alloc(N);
        gsl_vector* Eigenvalues = gsl_vector_alloc(N);

        double AverageTime = 0.0;
        for (int k = 0; k < K; k++)
        {   
            for (int i = 0; i < N; i++)
            {
                for (int j = i; j < N; j++)
                {
                    double rn = gsl_ran_gaussian(RandomNumberGenerator, 1.0);
                    gsl_matrix_set(A, i, j, rn);
                    gsl_matrix_set(A, j, i, rn);
                }
            }

            timespec start, end;
            clock_gettime(CLOCK_MONOTONIC_RAW, &start);

            gsl_eigen_symm(A, Eigenvalues, EigendecompositionWorkspace);

            clock_gettime(CLOCK_MONOTONIC_RAW, &end);
            double TimeElapsed = (double) ((1e9*end.tv_sec + end.tv_nsec) - (1e9*start.tv_sec + start.tv_nsec))/1.0e9;
            AverageTime += TimeElapsed/K;
            std::cout << "N = " << N << ", k = " << k << ", Time = " << TimeElapsed << std::endl;
        }
        OutputFile << N << " " << AverageTime << std::endl;

        gsl_matrix_free(A);
        gsl_eigen_symm_free(EigendecompositionWorkspace);
        gsl_vector_free(Eigenvalues);
    }

    return 0;
}

Mathematica

(* Symmetric real matrix + eigenvectors *)
Table[{NN, Mean[Table[(
     M = Table[Random[], {i, NN}, {j, NN}];
     M = M + Transpose[Conjugate[M]];
     AbsoluteTiming[Eigensystem[M]][[1]]
     ), {K, 10}]]
  }, {NN, Range[100, 1000, 100]}]

(* Symmetric real matrix *)
Table[{NN, Mean[Table[(
     M = Table[Random[], {i, NN}, {j, NN}];
     M = M + Transpose[Conjugate[M]];
     AbsoluteTiming[Eigenvalues[M]][[1]]
     ), {K, 10}]]
  }, {NN, Range[100, 1000, 100]}]

(* Asymmetric real matrix *)
Table[{NN, Mean[Table[(
     M = Table[Random[], {i, NN}, {j, NN}];
     AbsoluteTiming[Eigenvalues[M]][[1]]
     ), {K, 10}]]
  }, {NN, Range[100, 1000, 100]}]

(* Hermitian matrix *)
Table[{NN, Mean[Table[(
     M = Table[Random[] + I Random[], {i, NN}, {j, NN}];
     M = M + Transpose[Conjugate[M]];
     AbsoluteTiming[Eigenvalues[M]][[1]]
     ), {K, 10}]]
  }, {NN, Range[100, 1000, 100]}]

(* Random complex matrix *)
Table[{NN, Mean[Table[(
     M = Table[Random[] + I Random[], {i, NN}, {j, NN}];
     AbsoluteTiming[Eigenvalues[M]][[1]]
     ), {K, 10}]]
  }, {NN, Range[100, 1000, 100]}]

Ответ 3

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

Это может быть не так. В ссылка указано, что:

int gsl_eigen_symmv (gsl_matrix * A, gsl_vector * eval, gsl_matrix * evec, gsl_eigen_symmv_workspace * w)

Эта функция вычисляет собственные значения и собственные векторы вещественной симметричной матрицы А. Дополнительное рабочее пространство соответствующего размера должно быть указано в w. Диагональная и нижняя треугольная часть А разрушаются во время вычисление, но строгая верхняя треугольная часть не упоминается. Собственные значения сохраняются в векторе eval и неупорядочены. соответствующие собственные векторы сохраняются в столбцах матрицы evec. Например, собственный вектор в первом столбце соответствует первое собственное значение. Собственные векторы гарантированно взаимно ортогональной и нормированной на единицу величины.

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

На стороне MATLAB декомпозиция собственного значения может быть быстрее из-за многопоточного выполнения, как указано в эта ссылка:

Встроенная многопоточность

Линейная алгебра и числовые функции, такие как fft,\(mldivide), eig, svd и sort многопоточны в MATLAB. Многопоточные вычисления были включены по умолчанию в MATLAB начиная с версии 2008a. Эти функции автоматически выполняются на нескольких вычислительных потоках в один сеанс MATLAB, позволяющий им выполнять быстрее многоядерные машины. Кроме того, многие функции в Image Обработка Toolbox ™ многопоточна.

Чтобы проверить производительность MATLAB для одного ядра, вы можете отключить многопоточность на

Файл > Настройки > Общие > Многопоточность

в R2007a или новее, как указано здесь.