【问题标题】:Effect on performance when using objects in c++在 C++ 中使用对象时对性能的影响
【发布时间】:2010-02-04 21:10:41
【问题描述】:

我有一个用于 C++ 中背包的动态编程算法。当它被实现为一个函数并访问传递给它的变量时,它需要 22 秒才能在特定实例上运行。当我将它作为我的类 KnapsackInstance 的成员函数并让它使用作为该类的数据成员的变量时,它开始需要 37 秒才能运行。据我所知,只有访问成员函数会通过 vtable,所以我无法解释可能发生的情况。

这是函数的代码

int KnapsackInstance::dpSolve() {
 int i; // Current item number
 int d; // Current weight
 int * tbl; // Array of size weightLeft
 int toret;
 tbl = new int[weightLeft+1];
 if (!tbl) return -1;
 memset(tbl, 0, (weightLeft+1)*sizeof(int));
 for (i = 1; i <= numItems; ++i) {
  for (d = weightLeft; d >= 0; --d) {
   if (profitsWeights.at(i-1).second <= d) {
    /* Either add this item or don't */
    int v1 = profitsWeights.at(i-1).first + tbl[d-profitsWeights.at(i-1).second];
    int v2 = tbl[d];
    tbl[d] = (v1 < v2 ? v2 : v1);
   }
  }
 }
 toret = tbl[weightLeft];
 delete[] tbl;
 return toret;
}

tbl 是 DP 表的一列。我们从第一列开始,一直到最后一列。 ProfitWeights 变量是成对的向量,其中第一个元素是利润,第二个元素是权重。 toret 是要返回的值。

这里是原函数的代码:-

int dpSolve(vector<pair<int, int> > profitsWeights, int weightLeft, int numItems) {
 int i; // Current item number
 int d; // Current weight
 int * tbl; // Array of size weightLeft
 int toret;
 tbl = new int[weightLeft+1];
 if (!tbl) return -1;
 memset(tbl, 0, (weightLeft+1)*sizeof(int));
 for (i = 1; i <= numItems; ++i) {
  for (d = weightLeft; d >= 0; --d) {
   if (profitsWeights.at(i-1).second <= d) {
    /* Either add this item or don't */
    int v1 = profitsWeights.at(i-1).first + tbl[d-profitsWeights.at(i-1).second];
    int v2 = tbl[d];
    tbl[d] = (v1 < v2 ? v2 : v1);
   }
  }
 }
 toret = tbl[weightLeft];
 delete[] tbl;
 return toret;
}

这是在 Debian Lenny 上运行的,打开了 g++-4.3.2 和 -O3 -DNDEBUG

谢谢

【问题讨论】:

  • 我们还需要查看非类函数才能做出明智的评论。
  • 我想我们需要一种比较方法……而且,你有潜在的内存泄漏;如果抛出异常,delete[] tbl 将不会被调用。
  • 除了类成员函数与非成员函数的问题外,它可能有助于将profitWeight.at(i-1) 提升出for(d =...) 循环并将其分配给如果您的编译器尚未为您执行本地(寄存器)变量。

标签: c++ performance dynamic-programming compiler-optimization


【解决方案1】:

在典型实现中,成员函数接收指向实例数据的指针作为隐藏参数 (this)。因此,对成员数据的访问通常是通过指针来实现的,这可能会导致您看到速度变慢。

另一方面,只看一个版本的代码很难做更多的猜测。

看了两段代码后,我想我应该把成员函数写成这样:

int KnapsackInstance::dpSolve() {
    std::vector<int> tbl(weightLeft+1, 0);
    std::vector<pair<int, int> > weights(profitWeights);
    int v1;

    for (int i = 0; i <numItems; ++i) 
        for (int d = weightLeft; d >= 0; --d)
            if ((weights[i+1].second <= d) && 
                ((v1 = weights[i].first + tbl[d-weights[i-1].second])>tbl[d]))
                    tbl[d] = v1;
    return tbl[weightLeft];
}

【讨论】:

  • 虽然您对 (this) 被隐式传递是正确的,但在循环的第一次传递之后这应该无关紧要。想一想:一旦它被访问一次,它就应该被缓存。取消引用指针可以在许多处理器上通过 2 条指令完成:一条加载指针,一条以索引延迟模式加载数据。相比之下,加载变量通常只会产生一条指令。任何开销都不会引起注意,尤其是考虑到编译器优化和硬件级指令重新排序的激进性质。
  • @San Jacinto:你可能是对的——但话又说回来,也许不是。正如我在帖子中所暗示的那样,当时问题中只有一个版本的代码,所以我所能做的就是“一次它是成员函数,另一次不是”,以及这种情况下的唯一区别通过this 访问。我可以看到除了指针取消引用之外的其他可能性,因为它会导致瓶颈,例如通过this 进行的所有访问都阻止了一些并行执行。虽然很难猜...
  • 我为 ProfitWeights 向量做了一个本地别名,时间恢复正常。谢谢
猜你喜欢
  • 1970-01-01
  • 2010-09-11
  • 2013-01-12
  • 2012-12-10
  • 2015-12-03
  • 1970-01-01
  • 2017-02-15
  • 2013-08-30
  • 2021-03-09
相关资源
最近更新 更多