深层嵌套表达式的表达式模板性能

时间:2016-06-26 07:22:54

标签: c++ templates compiler-optimization expression-templates

我正在调查矩阵乘法中的表达式模板性能。我在矩阵乘法中展开循环,我发现原生双精度的性能等于表达式模板的性能,直到表达式的深度变得太大,表达式模板性能下降。这是代码:

#include <iostream>
#include <vector>
#include <chrono>

template<typename T>
struct Real
{
   typedef T RealType;

    Real() noexcept : m_real(0) {}
    inline explicit Real(RealType real) noexcept
        : m_real(real)
   {
   }

    inline RealType value() const noexcept
    {
        return m_real;
    }                                           

    template<typename Expr>
    void operator+=(const Expr& expr)
    {
        m_real += expr.value();
    }

    RealType m_real;
};

#define DEFINE_BINARY_OPERATOR(NAME, OP)      \
    template<typename Expr1, typename Expr2>                            \
    struct NAME                                                         \
    {                                                                   \
        typedef typename Expr1::RealType RealType;                      \
                                                                        \
        NAME() noexcept {}                                              \
        NAME(const Expr1& e1, const Expr2& e2) noexcept                 \
            : m_e1(e1), m_e2(e2) {}                                     \
                                                                        \
        inline RealType value() const noexcept                          \
        {                                                               \
            return m_e1.value() OP m_e2.value();                        \
        }                                                               \
                                                                        \
        Expr1 m_e1;                                                     \
        Expr2 m_e2;                                                     \
    };                                                                  \
    template<typename Expr1, typename Expr2>                            \
    inline decltype(auto) operator OP (const Expr1& e1, const Expr2& e2) noexcept\
    {                                                                   \
        return NAME<Expr1, Expr2>(e1, e2);                              \
    }                                                                   \

DEFINE_BINARY_OPERATOR(Multiply, *)
DEFINE_BINARY_OPERATOR(Add, +)
DEFINE_BINARY_OPERATOR(Subtract, -)
DEFINE_BINARY_OPERATOR(Divide, /)

template<typename T>
struct Matrix
{
    explicit Matrix(size_t size)
        : m_matrix(size, std::vector<T>(size))
    {
    }
    explicit Matrix(size_t size, const T& intialVal)
        : m_matrix(size, std::vector<T>(size, intialVal))
    {
    }
    std::vector<T>& operator[](size_t row) { return m_matrix[row]; }
    const std::vector<T>& operator[](size_t row) const { return m_matrix[row]; }
    size_t size() const { return m_matrix.size(); }

    std::vector<std::vector<T> > m_matrix;
};

#define MATRIX_MULT_KERNEL(N) m1[i][k+N] * m2[j][k+N]
#define MATRIX_MULT_ADD_KERNELS_4(N) \
   MATRIX_MULT_KERNEL(N) + MATRIX_MULT_KERNEL(N+1) + MATRIX_MULT_KERNEL(N+2) + MATRIX_MULT_KERNEL(N+3)

template<typename T>
Matrix<T> operator*(const Matrix<T>& m1, const Matrix<T>& m2)
{
    if (m1.size() != m2.size())
        throw std::runtime_error("wrong sizes");

    Matrix<T> m3(m1.size());
    for (size_t i = 0; i < m1.size(); ++i)
        for (size_t j = 0; j < m1.size(); ++j)
            for (size_t k = 0; k < m1.size(); k+=16)
            {
                auto v0 = MATRIX_MULT_ADD_KERNELS_4(0);
                auto v1 = MATRIX_MULT_ADD_KERNELS_4(4);
                auto v2 = MATRIX_MULT_ADD_KERNELS_4(8);
                auto v3 = MATRIX_MULT_ADD_KERNELS_4(12);
                // auto v4 = MATRIX_MULT_ADD_KERNELS_4(16);
                // auto v5 = MATRIX_MULT_ADD_KERNELS_4(20);
                // auto v6 = MATRIX_MULT_ADD_KERNELS_4(24);
                // auto v7 = MATRIX_MULT_ADD_KERNELS_4(28);
                auto expr = (v0 + v1 + v2 + v3);// + v4 + v5 + v6 + v7);
                m3[i][j] += expr;

            }
    return m3;
}

decltype(auto) now()
{
    return std::chrono::high_resolution_clock::now();
}

decltype(auto) milliseconds(const decltype(now())& start, const decltype(now())& end)
{
    return std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
}

int main()
{
    constexpr static const int SIZE = 1024;
    {
        Matrix<double> m1(SIZE, 1.0);
        Matrix<double> m2(SIZE, 1.0);
        auto begin = now();
        Matrix<double> m3 = m1 * m2;
        auto end = now();
        std::cout << milliseconds(begin, end) << "ms" << std::endl;
    }
    {
        Matrix<Real<double> > m1(SIZE, Real<double>(1.0));
        Matrix<Real<double> > m2(SIZE, Real<double>(1.0));
        auto begin = now();
        Matrix<Real<double> > m3 = m1 * m2;
        auto end = now();
        std::cout << milliseconds(begin, end) << "ms" << std::endl;
    }
}

如果我取消注释矩阵乘法循环中的代码,并将k递增32,则表达式模板需要三倍的时间。有谁知道为什么会发生这种情况?

我正在使用Intel Xeon E3-1225 V2在cygwin上编译GCC 5.4。

1 个答案:

答案 0 :(得分:0)

有三个原因。

首先,您要混合模板和宏。创建DEFINE_BINARY_OPERATOR宏等可能更容易,但您也会失去一些优化。可能有一种方法可以让编译器通过模板模板生成类,这可能允许编译器做一些不能通过宏的魔法。

其次,你正在使用二维向量。 C++ FAQ解释说,尝试使Matrix类看起来像一个数组的数组可能会导致性能问题。在这种情况下,您正在尝试构建一个三维矩阵;如果你使用()运算符为一般情况而不是特定情况的[][]运算符构建它可能会更便宜和更容易。同一部分解释了如何以更通用,更有效的方式实现Matrix类。

有一种数学方法可以使用一维数组“伪造”多维数组。 This question解释了如何,单一级别的间接也会减少一些开销。

第三,你可能不需要的那个vector类有很多开销。这可能是一个孤立的情况,最好只使用数组而不是vector,这只是因为除了更好的易用性之外,你没有从矢量中获得太多好处。

最后,不幸的是,该循环具有多项式复杂性。这意味着您从上面得到的任何性能问题都将成为立方体。对于一个小型阵列来说,这不是什么大问题,但对于一个大型阵列来说,这可能会变得昂贵。也许你需要打开或关闭一些优化标志来减少这个问题。