使用数组

时间:2017-11-08 13:50:41

标签: c++ performance abstraction

这是一个关于我在处理数组时在更好的性能和更清晰的代码(更好的抽象)之间进行选择的问题。我试着把它提炼成一个玩具的例子。

C ++特别擅长于在不损害性能的情况下允许抽象。问题是这是否可能在类似于下面的例子中。

考虑一个使用连续行主存储的普通任意大小矩阵类:

#include <cmath>
#include <cassert>

class Matrix {
    int nrow, ncol;
    double *data;
public:
    Matrix(int nrow, int ncol) : nrow(nrow), ncol(ncol), data(new double[nrow*ncol]) { }
    ~Matrix() { delete [] data; }

    int rows() const { return nrow; }
    int cols() const { return ncol; }

    double & operator [] (int i) { return data[i]; }

    double & operator () (int i, int j) { return data[i*ncol + j]; }
};

它具有2D索引operator (),以便于使用。对于连续访问,它也有operator [],但更好抽象的矩阵可能没有这个。

让我们实现一个函数,该函数采用n×2矩阵,本质上是一个2D向量列表,并对每个向量进行归一化。

明确的方式:

inline double veclen(double x, double y) {
    return std::sqrt(x*x + y*y);
}

void normalize(Matrix &mat) {
    assert(mat.cols() == 2); // some kind of check for correct input
    for (int i=0; i < mat.rows(); ++i) {
        double norm = veclen(mat(i,0), mat(i,1));
        mat(i,0) /= norm;
        mat(i,1) /= norm;
    }
}

快速但不太清晰的方式:

void normalize2(Matrix &mat) {
    assert(mat.cols() == 2);
    for (int i=0; i < mat.rows(); ++i) {
        double norm = veclen(mat[2*i], mat[2*i+1]);
        mat[2*i] /= norm;
        mat[2*i+1] /= norm;
    }
}

第二个版本(normalize2)有可能更快,因为它的编写方式很明显,循环的第二次迭代不会访问第一次迭代中计算的数据。因此,它可以更好地利用SIMD指令。 Looking at godbolt, this seems to be what happens(除非我误读了装配)。

在第一个版本(normalize)中,编译器无法知道输入矩阵不是n-by-1,这会导致重叠的数组访问。

问题:是否有可能以某种方式告诉编译器输入矩阵在normalize()中确实是n-by-2,以允许它优化到与在normalize2()

发表评论:

  • John Zwinck:我去做了基准测试。 normalize2()速度要快得多(2.4 vs 1.3秒),但只有在我删除assert宏或我定义NDEBUG时才会。这是-DNDEBUG的一个相当违反直觉的影响,不是吗?它会降低性能而不是改善性能。

  • Max:证据既是我链接的神韵输出,也是上述基准。我也对无法内联这两个函数的情况感兴趣(例如,因为它们位于单独的翻译单元中)。

  • Jarod42和bolov:这是我正在寻找的答案。由第一点提到的基准确认。不过,重要的是要知道如果一个人实现自己的assert(这正是我在我的应用程序中所做的)。

2 个答案:

答案 0 :(得分:1)

我相信模板可以让您实现性能和可读性。

通过模板化矩阵的大小(就像流行的数学库一样),让编译器在编译时知道很多信息。

我修改了你的小课程:

template<int R, int C>
class Matrix {
    double data[R * C] = {0.0};
public:
    Matrix() = default;

    int rows() const { return R; }
    int cols() const { return C; }
    int size() const { return R*C; }

    double & operator [] (int i) { return data[i]; }

    double & operator () (int row, int col) { return data[row*C + col]; }
};

inline double veclen(double x, double y) {
    return std::sqrt(x*x + y*y);
}

template<int R>
void normalize(Matrix<R, 2> &mat) {
    for (int i = 0; i < R; ++i) {
        double norm = veclen(mat(i, 0), mat(i, 1));
        mat(i, 0) /= norm;
        mat(i, 1) /= norm;
    }
}

template<int R>
void normalize2(Matrix<R, 2> &mat) {
    for (int i = 0; i < R; ++i) {
        double norm = veclen(mat[2 * i], mat[2 * i + 1]);
        mat[2 * i] /= norm;
        mat[2 * i + 1] /= norm;
    }
}

我也更喜欢将数据作为普通成员(=没有指针),因此您可以在矩阵构造期间选择内存(堆栈或堆)。

另外一个好处就是你现在可以确保在编译时normalize函数只接受n-by-2矩阵。

我没有在编译器资源管理器上测试我的代码,因为说实话,我无法破译asm。所以,是的,我声称我的版本更好而不确定;)

最后一句话:不要滚动自己的矩阵,使用库,如glm或eigen。

最后一个词²:如果您不知道该选择什么,请选择可读性。

答案 1 :(得分:1)

我接受的答案基本上是由@bolov和@Jared42在评论中给出的。由于他们没有发布,我会自己这样做。

为了让编译器知道矩阵的大小为n×2,将代码添加到函数的开头就足够了,当矩阵大小不正确时,代码的其余部分将无法访问。

例如,添加

if (mat.cols() != 2)
    throw std::runtime_error("Input array is not of expected shape.");

normalize()的开头允许它的运行速度与normalize2()一样快(1.3而不是基于clang 5.0的基准测试中的2.4秒)。

我们也可以添加一个assert(mat.cols() == 2),但这会产生违反直觉的影响,即在编译期间定义-DNDEBUG会使函数速度变慢(因为它会删除断言)。