Самый быстрый способ рассчитать определитель?

Я пишу библиотеку, в которой я хочу иметь некоторые базовые функциональные возможности матрицы NxN, которые не имеют никаких зависимостей, и это немного обучающий проект. Я сравниваю свое выступление с Эйгеном. Я смог сравняться и даже побить его производительность в паре фронтов с SSE2, а с AVX2 побить его на нескольких фронтах (он использует только SSE2, так что не удивительно).

Моя проблема заключается в том, что я использую метод исключения Гаусса, чтобы создать матрицу с верхней диагонализацией, а затем умножить диагональ, чтобы получить определитель. < 300, но после этого Эйген обдувает меня, и с увеличением матриц становится только хуже. Учитывая, что ко всей памяти обращаются последовательно, и разборка компилятора не выглядит ужасно, я не думаю, что это проблема оптимизации. Можно сделать больше оптимизаций, но временные характеристики выглядят гораздо более похожими на проблему сложности алгоритмического хронирования, или есть серьезное преимущество SSE, которого я не вижу. Просто немного развернув петли, я ничего не сделал для этого.

Есть ли лучший алгоритм для вычисления определителей?

Скалярный код

/*
Warning: Creates Temporaries!
*/
template<typename T, int ROW, int COLUMN> MML_INLINE T matrix<T, ROW, COLUMN>::determinant(void) const
{
/*
This method assumes square matrix
*/
assert(row() == col());
/*
We need to create a temporary
*/
matrix<T, ROW, COLUMN> temp(*this);
/*We convert the temporary to upper triangular form*/
uint N = row();
T det = T(1);
for (uint c = 0; c < N; ++c)
{
det = det*temp(c,c);
for (uint r = c + 1; r < N; ++r)
{
T ratio = temp(r, c) / temp(c, c);
for (uint k = c; k < N; k++)
{
temp(r, k) = temp(r, k) - ratio * temp(c, k);
}
}
}

return det;
}

AVX2

template<> float matrix<float>::determinant(void) const
{
/*
This method assumes square matrix
*/
assert(row() == col());
/*
We need to create a temporary
*/
matrix<float> temp(*this);
/*We convert the temporary to upper triangular form*/
float det = 1.0f;

const uint N = row();
const uint Nm8 = N - 8;
const uint Nm4 = N - 4;

uint c = 0;
for (; c < Nm8; ++c)
{
det *= temp(c, c);
float8 Diagonal = _mm256_set1_ps(temp(c, c));

for (uint r = c + 1; r < N;++r)
{
float8 ratio1 = _mm256_div_ps(_mm256_set1_ps(temp(r,c)), Diagonal);

uint k = c + 1;
for (; k < Nm8; k += 8)
{
float8 ref = _mm256_loadu_ps(temp._v + c*N + k);
float8 r0 = _mm256_loadu_ps(temp._v + r*N + k);

_mm256_storeu_ps(temp._v + r*N + k, _mm256_fmsub_ps(ratio1, ref, r0));
}

/*We go Scalar for the last few elements to handle non-multiples of 8*/
for (; k < N; ++k)
{
_mm_store_ss(temp._v + index(r, k), _mm_sub_ss(_mm_set_ss(temp(r, k)), _mm_mul_ss(_mm256_castps256_ps128(ratio1),_mm_set_ss(temp(c, k)))));
}
}
}

for (; c < Nm4; ++c)
{
det *= temp(c, c);
float4 Diagonal = _mm_set1_ps(temp(c, c));

for (uint r = c + 1; r < N; ++r)
{
float4 ratio = _mm_div_ps(_mm_set1_ps(temp[r*N + c]), Diagonal);
uint k = c + 1;
for (; k < Nm4; k += 4)
{
float4 ref = _mm_loadu_ps(temp._v + c*N + k);
float4 r0 = _mm_loadu_ps(temp._v + r*N + k);

_mm_storeu_ps(temp._v + r*N + k, _mm_sub_ps(r0, _mm_mul_ps(ref, ratio)));
}

float fratio = _mm_cvtss_f32(ratio);
for (; k < N; ++k)
{
temp(r, k) = temp(r, k) - fratio*temp(c, k);
}
}
}

for (; c < N; ++c)
{
det *= temp(c, c);
float Diagonal = temp(c, c);
for (uint r = c + 1; r < N; ++r)
{
float ratio = temp[r*N + c] / Diagonal;
for (uint k = c+1; k < N;++k)
{
temp(r, k) = temp(r, k) - ratio*temp(c, k);
}
}
}

return det;
}

3

Решение

Алгоритмы преобразования матрицы n на n в верхнюю (или нижнюю) треугольную форму путем исключения Гаусса обычно имеют сложность O (n ^ 3) (где ^ представляет «в степень»).

Существуют альтернативные подходы для вычисления детерминанта, такие как оценка набора собственных значений (определитель квадратной матрицы равен произведению ее собственных значений). Для общих матриц вычисление полного набора собственных значений также — практически — O (n ^ 3).

Однако теоретически вычисление множества собственных значений имеет сложность n^w где w находится между 2 и 2.376 — это означает, что для (намного) больших матриц это будет быстрее, чем использование исключения Гаусса. Взгляните на статью «Быстрая линейная алгебра стабильна» Джеймса Деммеля, Иоаны Думитриу и Ольги Хольц в Numerische Mathematik, том 108, выпуск 1, с. 59-91, ноябрь 2007 г. Если Эйген использует подход с меньшей сложностью чем O (n ^ 3) для более крупных матриц (я не знаю, никогда не было причин исследовать такие вещи), которые могли бы объяснить ваши наблюдения.

3

Другие решения

Ответ, как представляется, в большинстве мест использует Block LU Factorization для создания матрицы нижнего треугольника и верхнего треугольника в одном и том же пространстве памяти. Это ~ O (n ^ 2.5) в зависимости от размера блока, который вы используете.

Вот пример из Университета Райса, который объясняет алгоритм.

www.caam.rice.edu/~timwar/MA471F03/Lecture24.ppt

Деление на матрицу означает умножение на ее обратное.

Идея, по-видимому, состоит в том, чтобы значительно увеличить число n ^ 2 операций, но уменьшить число m ^ 3, что фактически снижает сложность алгоритма, поскольку m имеет фиксированный небольшой размер.

Я собираюсь потратить немного времени, чтобы написать это эффективно, так как для эффективного выполнения требуются алгоритмы «на месте», которые я еще не написал.

0

По вопросам рекламы ammmcru@yandex.ru
Adblock
detector