Какова стоимость вызова виртуальной функции неполиморфным способом?

У меня есть чистая абстрактная база и два производных класса:

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; } };

Звонит 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;
}

Ответ: ответ Энди 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 выдает аналогичный результат без встраивания). Есть ли что-то, что мешает компилятору встроиться в случае, если D1 а также D2 действительно происходят из 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 использует виртуальные функции для стирания типа), чем просто использование виртуальных функций (пока не могу проверить с помощью godbolt).

2 ответа

Решение

Стоит ли вызывать foo в точке A так же, как вызов не виртуальной функции-члена?

Да.

Или это дороже, чем если бы D1 и D2 не были бы получены из B?

Нет.

Компилятор будет разрешать эти вызовы функций статически, потому что они не выполняются через указатель или ссылку. Поскольку тип объектов, для которых вызывается функция, известен во время компиляции, компилятор знает, какая реализация foo() должен быть вызван.

Самое простое решение смотрит на внутренности компиляторов. В Clang мы находим canDevirtualizeMemberFunctionCall в 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;
}

Я считаю, что код (и сопутствующие комментарии) говорят сами за себя:)

Другие вопросы по тегам