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 并用它执行一些计算,而在另一种情况下,您使用的是全局常量,编译器大概可以直接替代。所以在一种情况下,生成的代码可能会这样做:

  1. 加载p.
  2. 调用abs(a - b),将结果命名为c
  3. 调用pow(c, p),将结果命名为d
  4. 调用d.sum()(不管是什么意思),将结果命名为e
  5. 计算1.0 / p,将结果命名为i
  6. 呼叫pow(e, i).

那是一堆库调用,而且库调用很慢。另外,pow 很慢。

当你使用全局常量时,编译器可以自己做一些计算。

  1. 调用abs(a - b),将结果命名为c
  2. pow(c, 2.0) 更有效地计算为 c * c,将结果命名为 d
  3. 调用d.sum(),将结果命名为e
  4. 1.0 / 2.00.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 的成员 - 相应地调整您的算法以利用此优化。