[英]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.