LTO, девиртуализация и виртуальные таблицы - PullRequest
15 голосов
/ 13 августа 2011

Сравнивая виртуальные функции в C ++ и виртуальные таблицы в C, компиляторы в целом (и для достаточно больших проектов) справляются с работой девиртуализации?

Наивно, кажется, что у виртуальных функций в 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, хотя он вызывается напрямую, то есть девиртуализирован. Эквивалент в C ++,

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 и C ++, хотя я не думаю, что добавленная семантика в версии C ++ в этом случае актуальна.

Обновление 2: Для девиртуализации в C компилятор должен доказать, что указатель функции в виртуальной таблице имеет определенное значение. Для девиртуализации в C ++ компилятор должен доказать, что объект является экземпляром определенного класса. Казалось бы, в первом случае доказательство сложнее. Тем не менее, виртуальные таблицы обычно модифицируются лишь в очень немногих местах, и что наиболее важно: просто потому, что они выглядят сложнее, не означает, что компиляторы не так хороши в этом (иначе вы можете поспорить, что ксоринг обычно быстрее, чем добавление двух целые числа).

Ответы [ 4 ]

9 голосов
/ 13 августа 2011

Разница в том, что в C ++ компилятор может гарантировать, что адрес виртуальной таблицы никогда не изменится.В C это просто еще один указатель, и вы можете нанести ему ущерб любого рода.

Однако виртуальные таблицы обычно модифицируются лишь в очень немногих местах

Компилятор не знает, что в C. В C ++ он может предположить , что он никогда не изменится.

4 голосов
/ 29 января 2014

Я попытался обобщить в http://hubicka.blogspot.ca/2014/01/devirtualization-in-c-part-2-low-level.html, почему родовые оптимизации трудно приспособить к девиртуализации.Ваш тестовый сценарий для меня встроен в GCC 4.8.1, но в чуть менее тривиальном тестовом примере, когда вы передаете указатель на свой «объект» из основного, это не будет.

Причина в том, что доказать, что виртуальная таблицауказатель в obj и сама виртуальная таблица не изменили модуль анализа псевдонимов должен отслеживать все возможные места, на которые вы можете указать.В нетривиальном коде, где вы передаете вещи за пределы текущего модуля компиляции, это часто проигранная игра.

C ++ дает вам больше информации о том, когда тип объекта может измениться и когда он известен.GCC использует это, и это будет намного больше использовать это в следующем выпуске.(Я тоже скоро напишу об этом).

3 голосов
/ 13 августа 2011

Да, если компилятор может определить точный тип виртуализированного типа, он может "девиртуализировать" (или даже встроить!) Вызов. Компилятор может сделать это только в том случае, если он может гарантировать, что, несмотря ни на что, эта функция необходима.
Основной проблемой является в основном многопоточность. В примере 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.
}
1 голос
/ 13 августа 2011

Это зависит от того, с чем вы сравниваете компилятор.По сравнению с оптимизацией по времени, профилю или по времени, компиляторы имеют меньше информации для использования.С меньшим количеством информации, оптимизация времени компиляции будет более консервативной (и делать меньше встраивания в целом).

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

...