【问题标题】:C++: Why accessing class data members is so slow compared to accessing global variables?C++:为什么访问类数据成员比访问全局变量慢?
【发布时间】:2015-01-16 15:26:57
【问题描述】:

我正在实现一个计算量很大的程序,在过去的几天里,我花了很多时间来熟悉面向对象的设计、设计模式和 SOLID 原则。我需要在我的程序中实现几个metrics,所以我设计了一个简单的界面来完成它:

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。

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

【问题讨论】:

  • 在你的 MinkowskiMetric 中复制一个 const
  • 另外,您是否正确复制了您的课程Metricdistance 前面确实有一个 virtual,对吧?
  • 另外,如果您追求速度,您可以尝试熟悉静态多态性的概念。我假设指标被多次调用,因此您可能希望将指标设为模板参数。
  • 我不认为 virtual 是问题,因为 = 0 意味着该方法应该被覆盖,使成员 p const 也无济于事。
  • 我敢打赌这是一个缓存未命中问题。指针间接和虚函数的使用让我怀疑每次必须通过对象的指针查找p 成员时,缓存都会丢失。将其与在间接级别少一级的全局变量中加载进行比较。

标签: c++ oop gcc optimization clang


【解决方案1】:

两个版本之间的区别在于,在一种情况下,编译器必须加载 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.0就是0.5pow(e, 0.5)可以翻译成更高效的sqrt(e)

【讨论】:

    【解决方案2】:

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

    ...
    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 在this talk 的中间讨论这个问题。

    【讨论】:

      【解决方案3】:

      如果您想在代码中使用 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 的成员的位置 - 相应地调整您的算法以利用此优化。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2012-02-26
        • 1970-01-01
        • 2014-11-17
        • 1970-01-01
        • 2021-12-10
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多