最大限度地减少繁琐的递归虚函数调用的时间开销

时间:2016-11-23 13:10:20

标签: c++ polymorphism

当然,虚函数调用会产生运行时开销。 但是,当我们有一个类成员树时,虚拟函数通常只会调用其成员的另一个虚函数(这可能是递归的),有没有办法最小化时间开销?

我写了一个简短的代码示例,演示了我的意思:

class Base {
  public:
    virtual int f(int i)                    { return i+1; }
};

class Derived: public Base {
    Base *another;
  public:
    Derived(Base *a)                        :another(a) { }
    int f(int i) override         { return another->f(i); }
};

int test(Base *x) {
  int ret=0; 
  for (int i=0; i<(1<<30); ++i) ret=x->f(ret);
  return ret;
}

int main() {
  Base x1;
  Derived x2(&x1);
  Derived x3(&x2);
  Derived x4(&x3);
  int r=test(&x1);
  printf("test: %d\n", r);
}

使用gcc 5.4.0编译,优化选项-O3,我得到以下运行时间:

test(&x1):  2.444s
test(&x2):  3.280s
test(&x3):  4.088s
test(&x4):  4.852s

那么,减少时间开销的最佳方法是什么?在我的特殊情况下,模板不是一个选项。

2 个答案:

答案 0 :(得分:1)

一般而言,与最终功能的实际工作量相比,虚拟呼叫的开销不大可能是显着的。

如果您已经进行了分析,并将连续的虚拟呼叫分派识别为 瓶颈,那么确实可以避免这种情况。实际上有多种解决方案。

第一个解决方案,也是最通用的,是在循环之前解决实际的函数链,而不是在每个步骤。

在C ++ 11中,这将涉及使用std::function<...>。这可以在不影响可定制性的情况下完成:

class Base {
public:
    virtual ~Base() {}
    virtual int f(int i)                    { return i+1; }

    virtual std::function<int(int)> f_dispatch() {
        return [this](int i) { return this->Base::f(i); };
    }
};

class Derived: public Base {
    Base* another;
public:
    Derived(Base* a): another(a) { }
    int f(int i) override { return another->f(i); }

    std::function<int(int)> f_dispatch() override {
        return another->f_dispatch();
    }
};

int test(Base* x) {
    auto f = x->f_dispatch();
    int ret = 0;
    for (int i = 0; i < (1<<30); ++i) { ret = f(ret); }
    return ret;
}

这个想法是std::function里面的lambda将封装一个完全虚拟化的路径。因此,您只剩下一个虚拟呼叫(std::function本身中的那个)。

另一种解决方案,不那么通用,但很多更适合优化,是扭转局面:不是在循环中调用虚函数,而是让虚函数执行循环(在其最内层) )。

这与以前的解决方案一样,只有一次解析虚拟调用链的优点;但是最重​​要的是它也意味着最终调用的函数内的整个循环可以被矢量化/优化/...

class Base {
public:
    virtual ~Base() {}
    virtual int f(int i)                    { return i+1; }

    virtual int f_loop(int n) { 
        int ret = 0;
        for (int i = 0; i < n; ++i) { ret = this->Base::f(i); }
        return ret;
    }
};

class Derived: public Base {
    Base* another;
public:
    Derived(Base* a): another(a) { }
    int f(int i) override { return another->f(i); }

    int f_loop(int n) override { return another->f_loop(n); }
};

int test(Base* x) {
    return x->f_loop(1<<30);
}

这个真的很好地优化了,正如LLVM IR所证明的那样(整个循环+虚拟调度+ ...已被优化):

; Function Attrs: norecurse nounwind uwtable
define i32 @main() #1 personality i8* bitcast (i32 (...)* @__gxx_personality_v0 to i8*) {
_Z4testP4Base.exit:
  %0 = tail call i32 (i8*, ...) @printf(i8* nonnull getelementptr inbounds ([10 x i8], [10 x i8]* @.str, i64 0, i64 0), i32 1073741824)
  ret i32 0
}

答案 1 :(得分:0)

通过分析场景,我们有一个virtual方法,该方法调用另一个virtual方法,该方法可以在不同的类上继续调用virtual方法。

这是一个强大的间接层,因为你有类似f(f2(f3(f4(x))))的东西,并且每个函数的行为不是在编译时决定的,而是在运行时通过vtable决定的。

另外你不想使用模板(这有什么要求?为什么你有这样的要求?)。

这听起来像一个XY问题,你想要达到什么目的?如果你试图通过连续应用泛型函数计算一个值,这可能是运行时的任何东西,很明显这会带来成本。

如果问题是完成virtual次调用的数量,那么您唯一可以尝试的是最小化数量,特别是因为virtual本身没有太多开销。您可以尝试重新设计结构,以便一次处理所有数据,例如:

virtual void foo(vector<int>& data) {
  std::transform(data.begin(), data.end(), data.begin(), [] (int value) { return value + 1; });
  other->foo(data);
}

这样可以最大限度地减少每层一个虚拟呼叫的数量,但当然会产生其他费用。如果您打算在任何情况下将计算值存储在某个地方,这可能会更好,这可能是也可能不是,因为您没有给出任何问题说明