繁体   English   中英

使用数组时的抽象与性能

[英]Abstraction vs. performance when working with arrays

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

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指令。 看着天幕,这似乎是发生的事情 (除非我误读了程序集)。

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

问题:是否可以某种方式告诉编译器输入矩阵在normalize()实际上是n-by-2,以使其可以优化到与normalize2()相同的水平?


解决意见:

  • John Zwinck:我去做了基准测试。 normalize2()的速度要快得多(2.4秒与1.3秒),但是仅当我删除assert宏或定义NDEBUG 时才如此 这是-DNDEBUG的相当违反直觉的效果,不是吗? 它降低了性能而不是提高了性能。

  • 马克斯:证据既是我链接的指标,也是上述基准。 对于这两个函数无法内联的情况,我也很感兴趣(例如,因为它们在单独的翻译单元中)。

  • Jarod42和bolov:这是我一直在寻找的答案。 由第一点提到的基准确认。 尽管如此,了解一个人实现自己的assert (这正是我在我的应用程序中所做的事情)的assert下,了解这一点仍然很重要。

我相信模板可以使您同时获得性能和可读性。

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

我修改了您的小课:

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;
    }
}

我还更喜欢将数据作为普通成员(=不带指针)放置,因此您可以在矩阵构造期间选择内存所在的位置(堆栈或堆)。

额外的好处是,您现在可以在编译时确定正常化函数仅接受n-by-2矩阵。

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

最后一句话:不要滚动自己的矩阵,而要使用glm或本征之类的库。

最后一句话²:如果您不知道要选择什么,则更喜欢可读性。

@bolov和@ Jared42在评论中基本上给出了我可以接受的答案。 由于他们没有发布,因此我会自己发布。

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

例如,添加

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

normalize()开始的位置,它的运行速度与normalize2()完全一样normalize2()在我使用clang 5.0的基准测试中为1.3秒而不是2.4秒)。

我们还可以添加assert(mat.cols() == 2) ,但这会产生反直觉的效果, -DNDEBUG在编译过程中定义-DNDEBUG会使函数变慢(因为它删除了断言)。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM