Какова стоимость вызова виртуальной функции не полиморфным способом?
У меня есть чистая абстрактная база и два производных класса:
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;
}
Ответ: ответ Andy Prowl - это правильный ответ, я просто хотел добавить сборку gcc (протестирован в godbolt: gcc-4.7 -O2 -march = native -std = С++ 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 будет inline, вызов функции:
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).
Ответы
Ответ 1
Вызывает ли вызов foo в точке A то же самое, что и вызов не виртуальной функции-члена?
Да.
Или это дороже, чем если бы D1 и D2 не были получены из B?
Нет.
Компилятор будет решать эти вызовы функций статически, потому что они не выполняются с помощью указателя или ссылки. Поскольку тип объектов, на которые вызывается функция, известен во время компиляции, компилятор знает, какая реализация foo()
должна быть вызвана.
Ответ 2
Самое простое решение - это внешний вид компиляторов. В 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;
}
Я считаю, что код (и сопровождающие комментарии) не требует пояснений:)