C++:为什么与访问全局变量相比,访问类数据成员如此缓慢

C++: Why accessing class data members is so slow compared to accessing global variables?

本文关键字:访问 数据成员 缓慢 为什么 全局变量 C++      更新时间:2023-10-16

我正在实现一个计算昂贵的程序,在过去的几天里,我花了很多时间熟悉面向对象设计,设计模式和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并使用它执行一些计算,而在另一种情况下,您使用的是全局常量,编译器可能可以直接替换该常量。因此,在一种情况下,生成的代码可能会这样做:

  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.5pow(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的成员 - 相应地调整算法以利用此优化。