当然,虚函数调用会产生运行时开销。 但是,当我们有一个类成员树时,虚拟函数通常只会调用其成员的另一个虚函数(这可能是递归的),有没有办法最小化时间开销?
我写了一个简短的代码示例,演示了我的意思:
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
那么,减少时间开销的最佳方法是什么?在我的特殊情况下,模板不是一个选项。
答案 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);
}
这样可以最大限度地减少每层一个虚拟呼叫的数量,但当然会产生其他费用。如果您打算在任何情况下将计算值存储在某个地方,这可能会更好,这可能是也可能不是,因为您没有给出任何问题说明