Подтвердить что ты не робот

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 обычно быстрее, чем добавление двух целые числа).

4b9b3361

Ответ 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).