Девиртуализация вызовов. Ч2

#опытным



В предыдущем посте мы столкнулись с невозможностью девиртуализировать функцию bar, т.к. мы не могли гарантировать отсутствие вызовов из других единиц трансляции.



Получается, что нам достаточно ограничить внешнее связывание? Рассмотрим в примерах дальше 😊



Запрет на внешнее связывание 1

Итак, мы ведь знаем, что для конкретной функции можно запретить внешнее связывание, например, с помощью static. Из живого примера:

// direct call!

static void bar(Base &da, Base &db)

{

// push  rbx

// mov rax, [rdi]

// mov   rbx, rsi

da.vmethod(); // call DerivedA::vmethod()

// mov   rdi, rbx

// pop   rbx

db.vmethod(); // jmp   DerivedB::vmethod()

}



Вызов функции bar - единственный в данной единице трансляции, с конкретными наследниками Base. Следовательно, мы можем доказать П.2, П.4, П.3 (терминология из первой части).



Кстати, П.2 может быть доказан лишь частично! Например, bar можно вызывать с разными аргументами, тогда оптимизация будет совершена лишь частично:

// indirect + direct call

static void bar(Base &da, Base &db)

{

// push  rbx

// mov rax, [rdi]

// mov   rbx, rsi

da.vmethod(); // call  [[rax]]

// mov   rdi, rbx

// pop   rbx

db.vmethod(); // jmp   DerivedB::vmethod()

}



В данном случае, с учетом всех наборов аргументов при вызове foo, только второй vmethod может быть оптимизирован.



Запрет на внешнее связывание 2

В предыдущих способах можно заметить, что сложности возникают с доказательством П.2 и П.4. Компилятор опасается, что в других единицах трансляции появятся либо новые перегрузки, либо будут вызваны функции с объектами других наследников полиморфных классов.



Учитывая особенности сборки проекта, разработчик может намеренно сообщить компилятору, что других единиц трансляции не будет. В частности, для LLVM Clang можно применить следующие опции:

-flto -fwhole-program-vtables -fvisibility=hidden



В GCC можно вообще указать, что компилируемая единица и есть вся программа с помощью флага:

-fwhole-program



Он буквально разрешает считать, что компилятор знает ВСЕ известные перегрузки и их вызовы. Короче, отметит все функции ключевым словом static: живой пример.



Запрет на внешнее связывание 3

Еще один способ показать компилятору, что новых полиморфных перегрузок не появится. Можно использовать unnamed namespace:

namespace

{

struct Base

{

virtual void vmethod();

};



struct Derived : public Base

{

void vmethod() override;

};

}



Теперь данное семейство полиморфных классов будет скрыто от других единиц трансляции, что доказывает компилятору П.3 и П.4, а так же П.2 по месту требования.



Вот такими несложными действиями можно сократить количество обращений к таблице виртуальных методов и ускорить выполнение вашего приложения 😉



#cppcore #hardcore #howitworks