LTO, девиртуализация и виртуальные таблицы
Сравнение виртуальных функций в С++ и виртуальных таблицах в C, делают ли компиляторы в целом (и для достаточно больших проектов) как хорошая работа при девиртуализации?
Наивно, похоже, что виртуальные функции в С++ имеют немного больше семантики, поэтому может быть проще девиртуализировать.
Обновление: Mooing Duck, упомянутый в стиле девиртуализированных функций. Быстрая проверка показывает пропущенные оптимизации с виртуальными таблицами:
struct vtab {
int (*f)();
};
struct obj {
struct vtab *vtab;
int data;
};
int f()
{
return 5;
}
int main()
{
struct vtab vtab = {f};
struct obj obj = {&vtab, 10};
printf("%d\n", obj.vtab->f());
}
Мой GCC не будет встраивать f, хотя он вызывается непосредственно, т.е. девиртуализован. Эквивалент в С++,
class A
{
public:
virtual int f() = 0;
};
class B
{
public:
int f() {return 5;}
};
int main()
{
B b;
printf("%d\n", b.f());
}
делает даже встроенный f. Итак, есть первое отличие между C и С++, хотя я не думаю, что добавленная семантика в версии С++ имеет значение в этом случае.
Обновление 2: для девиртуализации в C компилятор должен доказать, что указатель функции в виртуальной таблице имеет определенное значение. Для девиртуализации в С++ компилятор должен доказать, что объект является экземпляром определенного класса. Казалось бы, доказательство сложнее в первом случае. Тем не менее, виртуальные таблицы обычно изменяются только в очень немногих местах, а самое главное: только потому, что они выглядят сложнее, это не значит, что компиляторы не так хороши в этом (иначе вы могли бы утверждать, что xoring обычно быстрее, чем добавление двух целые числа).
Ответы
Ответ 1
Разница в том, что в С++ компилятор может гарантировать, что адрес виртуальной таблицы никогда не изменится. В C это просто другой указатель, и вы можете нанести ему какой-то хаос.
Однако, виртуальные таблицы обычно изменяются только в очень немногих местах
Компилятор не знает, что в C. В С++ он может предположить, что он никогда не изменяется.
Ответ 2
Я попытался обобщить в http://hubicka.blogspot.ca/2014/01/devirtualization-in-c-part-2-low-level.html, почему универсальные оптимизации имеют трудное время для девиртуализации. Ваш тестовый ввод встраивается для меня с GCC 4.8.1, но в немного меньшей тривиальной тестовой таблице, где вы передаете указатель на свой "объект" из основного, это не будет.
Причина в том, что для доказательства того, что указатель виртуальной таблицы в obj и виртуальной таблице сам не изменился, модуль анализа псевдонима должен отслеживать все возможные места, на которые вы можете указать. В нетривиальном коде, где вы передаете вещи за пределами текущего блока компиляции, это часто потерянная игра.
С++ дает вам больше информации о том, когда тип объекта может измениться и когда он известен. GCC использует его, и он будет намного больше использовать его в следующей версии. (Я тоже скоро напишу об этом).
Ответ 3
Да, если для компилятора возможно вывести точный тип виртуализованного типа, он может "девиртуализировать" (или даже встроить!) вызов. Компилятор может сделать это только в том случае, если он может гарантировать, что независимо от того, что это за функция.
Основная проблема заключается в том, что в основном нарезание резьбы. В примере на С++ гарантии сохраняются даже в потоковой среде. В C это невозможно гарантировать, поскольку объект может быть захвачен другим потоком/процессом и перезаписан (намеренно или иначе), поэтому функция никогда не "девиртуализируется" или не вызывается напрямую. В C поиск всегда будет там.
struct A {
virtual void func() {std::cout << "A";};
}
struct B : A {
virtual void func() {std::cout << "B";}
}
int main() {
B b;
b.func(); //this will inline in optimized builds.
}
Ответ 4
Это зависит от того, к чему вы сравниваете вложение компилятора. По сравнению с привязкой времени или профилем, ориентированным или просто во времени, у компиляторов меньше информации для использования. При меньших объемах информации оптимизация времени компиляции будет более консервативной (и делать меньше вложения в целом).
Компилятор по-прежнему будет довольно приличным при встраивании виртуальных функций, так как он эквивалентен наложению вызовов указателей на функцию (например, когда вы передаете свободную функцию функции алгоритма STL, например sort
или for_each
).