C++:为什么与访问全局变量相比,访问类数据成员如此缓慢
C++: Why accessing class data members is so slow compared to accessing global variables?
我正在实现一个计算昂贵的程序,在过去的几天里,我花了很多时间熟悉面向对象设计,设计模式和SOLID原则。我需要在我的程序中实现几个指标,所以我设计了一个简单的界面来完成它:
class Metric {
typedef ... Vector;
virtual ~Metric() {}
virtual double distance(const Vector& a, const Vector& b) const = 0;
};
我实施的第一个指标是闵可夫斯基指标,
class MinkowskiMetric : public Metric {
public:
MinkowskiMetric(double p) : p(p) {}
double distance(const Vector& a, const Vector& b) const {
const double POW = this->p; /** hot spot */
return std::pow((std::pow(std::abs(a - b), POW)).sum(), 1.0 / POW);
private:
const double p;
};
使用此实现,代码运行得非常慢,有人尝试使用全局变量而不是访问数据成员,我的最后一个实现没有完成工作,但看起来像这样。
namespace parameters {
const double p = 2.0; /** for instance */
}
热点线如下所示:
...
const double POW = parameters::p; /** hot spot */
return ...
只是进行这种更改,代码在我的机器中的运行速度至少提高了 275 倍,在 Ubuntu 14.04.1 中使用带有优化标志的 gcc-4.8 或 clang-3.4。
这是一个常见的陷阱吗?有什么办法吗?我只是错过了什么吗?
两个版本之间的区别在于,在一种情况下,编译器必须加载p
并使用它执行一些计算,而在另一种情况下,您使用的是全局常量,编译器可能可以直接替换该常量。因此,在一种情况下,生成的代码可能会这样做:
- 加载
p
. - 调用
abs(a - b)
,将结果命名为c
- 调用
pow(c, p)
,将结果命名为d
- 调用
d.sum()
(无论这意味着什么),将结果命名为e
- 计算
1.0 / p
,将结果命名为i
- 呼叫
pow(e, i)
。
那是一堆库调用,而库调用很慢。此外,pow
很慢。
使用全局常量时,编译器可以自行执行一些计算。
- 调用
abs(a - b)
,将结果命名为c
。 -
pow(c, 2.0)
计算效率更高,c * c
,将结果命名为d
- 调用
d.sum()
,将结果命名为e
-
1.0 / 2.0
是0.5
,pow(e, 0.5)
可以翻译成更高效的sqrt(e)
。
让我们来看看这里发生了什么:
...
Metric *metric = new MinkowskiMetric(2.0);
metric->distance(a, b);
由于distance
是一个虚函数,运行时必须查找要在虚拟函数表指针中加载的metric
指针的地址,然后使用该地址查找对象的distance
函数的地址。
这可能是接下来发生的事情的附带情况:
double distance(const Vector& a, const Vector& b) const {
const double POW = this->p; /** hot spot */
然后,该函数必须查找this
指针的地址(恰好在此处明确说明),以便知道从哪个位置加载 p
的值。将其与使用全局变量的版本进行比较:
double distance(const Vector& a, const Vector& b) const {
const double POW = parameters::p; /** hot spot */
...
namespace parameters {
const double p = 2.0; /** for instance */
}
此版本的p
将始终位于同一地址,因此加载其值只会是单个操作,并删除几乎肯定会导致缓存未命中并导致CPU阻塞等待从RAM加载数据的间接级别。
那么如何避免这种情况呢?尝试尽可能多地分配堆栈上的对象。这启用了称为空间局部性的参考位置,这意味着您的数据在需要加载时更有可能存在于 CPU 的缓存中。你可以看到赫伯·萨特(Herb Sutter)在这次演讲中讨论了这个问题。
在应该有点性能的代码中使用OOP,你仍然需要尽量减少内存访问量。这意味着设计的变化。以您的示例为例(假设您正在评估指标几次):
double MinkowskiMetric::distance(const Vector& a, const Vector& b) const {
const double POW = this->p; /** hot spot */
return std::pow((std::pow(std::abs(a - b), POW)).sum(), 1.0 / POW);
}
可以变成
template<class VectorIter, class OutIter>
void MinkowskiMetric::distance(VectorIter aBegin, VectorIter aEnd, VectorIter bBegin, OutIter rBegin) const {
const double pow = this->p, powInv = 1.0 / pow;
while(aBegin != aEnd) {
Vector a = *aBegin++;
Vector b = *bBegin++;
*rBegin++ = std::pow((std::pow(std::abs(a - b), pow)).sum(), powInv);
}
}
现在,您将为一组Vector
对访问一次虚拟函数的位置和this
的成员 - 相应地调整算法以利用此优化。
- 用于访问容器<T>数据成员的正确 API
- 使用指针访问数组中的对象数据成员
- 友元函数无法访问私有数据成员 (c++)
- 在类 A 中创建类型为 B 类的向量 - 访问数据 [C++] [成员在两个类中都是私有的]
- 访问数据成员(本身是对象)的数据成员,就好像它们是类成员一样
- 使公共数据成员在C++中无法访问
- 有没有办法在C++中循环访问对象的不同数据成员
- 如何在C++中使用类对象访问指针数据成员
- 通过指针算法访问结构数据成员
- 是否可以访问类数据成员并在析构函数中对它们执行操作?
- 如何在 c++ 中访问类的私有数据成员
- 现代C++编译器是否优化了对类中同一数据成员的重复访问?
- 无法访问派生类函数内的基类的受保护数据成员
- 我将如何访问类中结构的数据成员
- C++istream运算符重载-即使声明为友元,也无法访问数据成员
- 访问数据成员的继承成员函数
- 可以通过d总线访问数据成员和指向对象的指针吗?
- 隔离类的并发/非并发访问数据成员
- 通过指针访问数据成员的代价
- C++无论如何都可以在不知道名称的情况下访问数据成员