9

純粋な抽象ベースと 2 つの派生クラスがあります。

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 -march=native -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*)

これにより、さらに最適化が可能になる可能性があります。派生しない場合D1それらD2同等ではないとB思います(少なくとも、これらの最適化を使用したこのバージョンの gcc では、-O3 はインライン化なしで同様の出力を生成しました)。およびが から派生する場合に、コンパイラがインライン化するのを妨げるものはありますか?D1D2B

「修正」:デリゲートを使用します (別名、仮想関数を自分で再実装します):

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ただし、ベクターへのアクセスは、単に仮想関数を使用するよりも遅くなる (または、型の消去に仮想関数を使用するため、少なくとも同じくらい高速になる) と思います (まだ Godbolt でテストできません)。

4

2 に答える 2

8

ポイント A で foo を呼び出すと、非仮想メンバー関数の呼び出しと同じコストがかかりますか?

はい。

それとも、D1 と D2 が B から派生していない場合よりもコストが高くなりますか?

いいえ。

これらの関数呼び出しはポインターまたは参照を介して実行されないため、コンパイラーはこれらの関数呼び出しを静的に解決します。関数が呼び出されるオブジェクトの型はコンパイル時に認識されるため、コンパイラはどの実装をfoo()呼び出す必要があるかを認識します。

于 2013-02-17T15:54:20.377 に答える
4

最も簡単な解決策は、コンパイラの内部を調べることです。ClangcanDevirtualizeMemberFunctionCallでは、lib/CodeGen/CGClass.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;
}

コード(および付随するコメント)は自明だと思います:)

于 2013-02-17T16:58:45.580 に答える