C++:为什么访问 class 数据成员比访问全局变量要慢?
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;
};
我实施的第一个指标是 Minkowski 指标,
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 倍,使用 gcc-4.8 或 clang-3.4 并在 Ubuntu 14.04.1.
中使用优化标志
这是一个常见的问题吗?
有什么办法吗?
我是不是漏掉了什么?
让我们看看这里发生了什么:
...
Metric *metric = new MinkowskiMetric(2.0);
metric->distance(a, b);
由于distance
是一个虚函数,运行时必须查找metric
指针的地址以加载到虚函数table指针中,然后使用它来查找对象的 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 在讨论这个问题this talk.
中间
这两个版本的区别在于,在一种情况下,编译器必须加载 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)
。
如果您想在应该具有一定性能的代码中使用 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
的成员 - 相应地调整您的算法以利用此优化。
我正在实施一个计算量大的程序,在过去的几天里,我花了很多时间来熟悉面向对象的设计、设计模式和 SOLID 原则。我需要在我的程序中实现几个 指标 所以我设计了一个简单的界面来完成它:
class Metric {
typedef ... Vector;
virtual ~Metric() {}
virtual double distance(const Vector& a, const Vector& b) const = 0;
};
我实施的第一个指标是 Minkowski 指标,
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 倍,使用 gcc-4.8 或 clang-3.4 并在 Ubuntu 14.04.1.
中使用优化标志这是一个常见的问题吗? 有什么办法吗? 我是不是漏掉了什么?
让我们看看这里发生了什么:
...
Metric *metric = new MinkowskiMetric(2.0);
metric->distance(a, b);
由于distance
是一个虚函数,运行时必须查找metric
指针的地址以加载到虚函数table指针中,然后使用它来查找对象的 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 在讨论这个问题this talk.
中间这两个版本的区别在于,在一种情况下,编译器必须加载 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)
。
如果您想在应该具有一定性能的代码中使用 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
的成员 - 相应地调整您的算法以利用此优化。