什么是计算行列式的最快方法?

6

我正在编写一个库,希望实现一些基本的NxN矩阵功能,不需要任何依赖项,这是一个学习项目。我正在将自己的性能与Eigen进行比较。使用SSE2在某些方面甚至能够超越它,并且使用AVX2在很多方面都能超越它(它只使用SSE2,所以并不是非常令人惊讶)。

我的问题是,我正在使用高斯消元来创建上三角矩阵,然后将对角线相乘以获得行列式。对于N < 300,我能够超过Eigen,但在此之后,Eigen的性能就远远超过我,而且随着矩阵变得更大,情况变得越来越糟。由于所有内存都是按顺序访问的,并且编译器的反汇编看起来并不可怕,我认为这不是优化问题。

还有更多可以优化的地方,但时间看起来更像是算法时间复杂度问题,或者存在我没有看到的主要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;
}

2
既然你显然已经准备好深入细节,而且Eigen是开源的...为什么不看看它做了什么,或者逐步了解它呢?[https://github.com/RLovelett/eigen/search?utf8=%E2%9C%93&q=determinant] - HostileFork says dont trust SE
他们的方法对我来说没有太多意义,这使得我很难适应我的库的操作方式。如果我理解了它背后的数学推理,我就能够很容易地适应它。我认为这与部分主元素有关,我正在开始研究它。其他的方法对我来说都很有道理,但这是我第一次无法理解其背后的方法论。总的来说,只是好奇是否有其他人有“最佳方法”的想法。即使在发布这个问题之后,我仍然在非常努力地研究它,并且会在找到更好的方法时发布我的代码。 - Chase R Lewis
这可能对你有兴趣:https://dev59.com/8n3aa4cB1Zd3GeqPhLp_ - Z boson
我认为你需要进行旋转操作来处理行列式为-1的矩阵,例如(0 1; 1 0),否则你的方法可能会失败。 - dmuir
是的,我正在考虑在那之前要使用什么算法。 - Chase R Lewis
2个回答

5

高斯消元法将n乘n矩阵化为上(或下)三角形的算法通常具有O(n^3)的复杂度(其中^表示“的幂”)。

计算行列式的替代方法是评估特征值集合(方阵的行列式等于其特征值的乘积)。对于一般矩阵,完整特征值集合的计算也是 - 实际上 - O(n^3)。

然而,在理论上,计算特征值集合的复杂度为n^w,其中w介于2到2.376之间 - 这意味着对于(更大的)矩阵来说,比使用高斯消元法更快。请参阅James Demmel、Ioana Dumitriu和Olga Holtz在Numerische Mathematik杂志第108卷第1期59-91页中发表的文章“快速线性代数是稳定的”。如果Eigen在处理较大矩阵时使用复杂度小于O(n^3)的方法(我不知道,从未有过调查这类事情的理由),那么这可以解释您的观察结果。


谢谢,那篇论文很有趣。我认为“块LU分解”是Eigen使用8x8子块的方法。该论文指出时间复杂度约为O(n^2.5)。我还会更深入地研究特征值选项。 - Chase R Lewis

1

大多数地方似乎使用块LU分解来创建下三角矩阵和上三角矩阵,并将它们存储在同一内存空间中。这需要取决于所使用的块的大小,时间复杂度为~O(n^2.5)。

以下是来自莱斯大学的幻灯片,详细解释了该算法。

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

矩阵除法意味着乘以其逆矩阵。

这个想法似乎是为了显著增加n^2操作的数量,但减少m^3的数量,从而降低算法的复杂度,因为m是固定的小尺寸。

写出高效的内容需要“原地”算法,我还没有编写。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接