C ++:与访问全局变量相比,为什么访问类数据成员的速度太慢?

时间:2015-01-16 15:26:57

标签: c++ oop gcc optimization clang

我正在实施一个计算成本昂贵的程序,在过去的几天里,我花了很多时间熟悉面向对象的设计,设计模式和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倍,在Ubuntu 14.04.1中使用gcc-4.8或clang-3.4和优化标志。

这是一个常见的陷阱吗? 它有什么办法吗? 我只是错过了一些东西吗?

3 个答案:

答案 0 :(得分:5)

两个版本之间的区别在于,在一种情况下,编译器必须加载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)
  7. 这是一堆库调用,库调用很慢。此外,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)

答案 1 :(得分:2)

让我们来看看这里发生了什么:

...
Metric *metric = new MinkowskiMetric(2.0);
metric->distance(a, b);

由于distance是一个虚函数,运行时必须查找要加载到虚函数表指针中的metric指针的地址,然后用它来查找{{1}的地址1}}你的对象的功能。

这可能是下一步发生的事件:

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 */ } 总是会存在于同一个地址,因此加载它的值只会是一个单独的操作并删除一个间接级别,这几乎肯定会导致缓存未命中和导致CPU阻塞等待从RAM加载数据。

那么你怎么能避免这种情况呢?尝试尽可能在堆栈上分配对象。这使得引用的局部性称为空间局部性,这意味着当数据需要加载时,您的数据更有可能存在于CPU的缓存中。您可以看到Herb Sutter在{{3}的中间讨论此问题}。

答案 2 :(得分:1)

如果你想在代码中使用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);
    }
}

现在,您只需为一组this对访问虚拟函数的位置和Vector的成员一次 - 相应地调整算法以利用此优化。< / p>