当前位置: 首页 > 知识库问答 >
问题:

以非多态方式调用虚拟函数的代价是什么?

端木昱
2023-03-14

我有一个纯抽象基和两个派生类:

struct B { virtual void foo() = 0; };
struct D1 : B { void foo() override { cout << "D1::foo()" << endl; } };
struct D2 : B { void foo() override { cout << "D1::foo()" << endl; } };

在点A中调用foo的成本是否与调用非虚拟成员函数的成本相同?或者,如果D1和D2不是从B中派生出来的,那么它的成本会更高吗?

int main() {
 D1 d1; D2 d2; 
 std::vector<B*> v = { &d1, &d2 };

 d1.foo(); d2.foo(); // Point A (polymorphism not necessary)
 for(auto&& i : v) i->foo(); // Polymorphism necessary.

 return 0;
}

答:Andy Prowl的答案是正确的答案,我只是想添加gcc的组装输出(在Godbolt中测试:gcc-4.7-O2-mar=local-std=c 11)。直接函数调用的成本是:

mov rdi, rsp
call    D1::foo()
mov rdi, rbp
call    D2::foo()

对于多态调用:

mov rdi, QWORD PTR [rbx]
mov rax, QWORD PTR [rdi]
call    [QWORD PTR [rax]]
mov rdi, QWORD PTR [rbx+8]
mov rax, QWORD PTR [rdi]
call    [QWORD PTR [rax]]

但是,如果对象不是从B派生的,而您只是执行直接调用,那么gcc将内联函数调用:

mov esi, OFFSET FLAT:.LC0
mov edi, OFFSET FLAT:std::cout
call    std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)

如果D1D2不派生自B,这可以实现进一步的优化,所以我想不,它们不是等价的(至少对于具有这些优化的这个版本的gcc,-O3在没有内联的情况下产生了类似的输出)。在D1D2确实派生自B的情况下,是否有什么东西阻止编译器内联?

“修复”:使用委托(也称为自己重新实现虚拟功能):

struct DG { // Delegate
 std::function<void(void)> foo;
 template<class C> DG(C&& c) { foo = [&](void){c.foo();}; }
};

然后创建一个委托向量:

std::vector<DG> v = { d1, d2 };

如果您以非多态方式访问方法,这允许内联。但是,我想访问向量会比仅使用虚拟函数慢(或者至少一样快,因为std::function使用虚拟函数进行类型擦除)(还不能用戈德博尔特测试)。

共有2个答案

屈昊天
2023-03-14

最简单的解决方案是查看编译器内部。在Clang中,我们可以在lib/CodeGen/CGClass中找到canDevirtualizeMemberFunctionCall。cpp:

/// canDevirtualizeMemberFunctionCall - Checks whether the given virtual member
/// function call on the given expr can be devirtualized.
static bool canDevirtualizeMemberFunctionCall(const Expr *Base, 
                                              const CXXMethodDecl *MD) {
  // If the most derived class is marked final, we know that no subclass can
  // override this member function and so we can devirtualize it. For example:
  //
  // struct A { virtual void f(); }
  // struct B final : A { };
  //
  // void f(B *b) {
  //   b->f();
  // }
  //
  const CXXRecordDecl *MostDerivedClassDecl = getMostDerivedClassDecl(Base);
  if (MostDerivedClassDecl->hasAttr<FinalAttr>())
    return true;

  // If the member function is marked 'final', we know that it can't be
  // overridden and can therefore devirtualize it.
  if (MD->hasAttr<FinalAttr>())
    return true;

  // Similarly, if the class itself is marked 'final' it can't be overridden
  // and we can therefore devirtualize the member function call.
  if (MD->getParent()->hasAttr<FinalAttr>())
    return true;

  Base = skipNoOpCastsAndParens(Base);
  if (const DeclRefExpr *DRE = dyn_cast<DeclRefExpr>(Base)) {
    if (const VarDecl *VD = dyn_cast<VarDecl>(DRE->getDecl())) {
      // This is a record decl. We know the type and can devirtualize it.
      return VD->getType()->isRecordType();
    }

    return false;
  }

  // We can always devirtualize calls on temporary object expressions.
  if (isa<CXXConstructExpr>(Base))
    return true;

  // And calls on bound temporaries.
  if (isa<CXXBindTemporaryExpr>(Base))
    return true;

  // Check if this is a call expr that returns a record type.
  if (const CallExpr *CE = dyn_cast<CallExpr>(Base))
    return CE->getCallReturnType()->isRecordType();

  // We can't devirtualize the call.
  return false;
}

我相信代码(以及附带的注释)是不言自明的:)

勾安翔
2023-03-14

在点A中调用foo的成本是否与调用非虚拟成员函数的成本相同?

是的。

或者,如果D1和D2不是从B中派生出来的,那么它的成本会更高吗?

不。

编译器将静态解析这些函数调用,因为它们不是通过指针或引用执行的。由于调用函数的对象的类型在编译时是已知的,因此编译器知道必须调用foo()的哪个实现。

 类似资料:
  • 问题内容: 我已经在某些计算机科学测试中看到了下一段,希望我能在这里对它的含义有一个很好的解释,因为我用它搜索了一个小时,却找不到任何东西。 “当我们说Java语言具有 虚拟方法调用时 ,是指在Java应用程序中,执行的方法由运行时的对象类型决定” 这是什么意思?谁能更好地解释它? 问题答案: 这些行的作者使用的C ++术语。 更好的术语是动态绑定/动态调度。 这意味着,对象的动态类型是“选择”将

  • 我读到虚拟析构函数必须在具有虚拟方法的类中声明。我只是不明白为什么必须宣布它们是虚拟的。我从下面的例子中知道为什么我们需要虚拟析构函数。我只是想知道为什么编译器不为我们管理虚拟析构函数。关于虚拟析构函数的工作,有什么我需要知道的吗?以下示例显示,如果析构函数未声明为虚拟,则派生类的析构函数不会被调用,这是为什么?

  • 我理解,只要你有一个多态基类,基类就应该定义一个虚拟析构函数。因此,当删除指向派生类对象的基类指针时,它将首先调用派生类的析构函数。如果我错了,请纠正我。 此外,如果基类析构函数是非虚拟的,那么删除指向派生对象的基类指针将是未定义的行为。如果我错了也纠正我。 所以我的问题是:为什么当基类析构函数非虚拟时,对象不会被正确销毁? 我假设这是因为虚拟函数具有某种表,每当调用虚拟函数时都会记住和查阅该表。

  • 我想“禁用”第一个,类似于当我声明一个类的方法为纯虚的时候。

  • 我想知道为什么我们不应该重写非虚拟函数?

  • 我试图在Rust中实现一些类似于类中的C虚拟函数的东西,我会有一个带有数据的基本结构,然后我会保留一些未定义的函数,如以下示例: 我试图用函数指针来实现它,但是没有成功。我可以在A的函数中使用trait,并在另一个类中实现A,但是我会丢失结构的数据。什么最好(最快?)Rust中实现这种东西的方式?