Существуют ли какие-либо издержки / затраты на виртуальное наследование в C ++ при вызове не виртуального базового метода? - PullRequest
22 голосов
/ 05 апреля 2011

Имеет ли использование виртуального наследования в C ++ штраф за время выполнения в скомпилированном коде, когда мы вызываем член обычной функции из его базового класса?Пример кода:

class A {
    public:
        void foo(void) {}
};
class B : virtual public A {};
class C : virtual public A {};
class D : public B, public C {};

// ...

D bar;
bar.foo ();

Ответы [ 6 ]

18 голосов
/ 05 апреля 2011

Да, если вы вызываете функцию-член через указатель или ссылку, и компилятор не может с абсолютной уверенностью определить, на какой тип объекта указывает или ссылается указатель или ссылка. Например, рассмотрим:

void f(B* p) { p->foo(); }

void g()
{
    D bar;
    f(&bar);
}

Предполагая, что вызов f не является встроенным, компилятору необходимо сгенерировать код, чтобы найти местоположение подобъекта виртуального базового класса A, чтобы вызвать foo. Обычно этот поиск включает проверку vptr / vtable.

Если компилятор знает тип объекта, для которого вы вызываете функцию, хотя (как это имеет место в вашем примере), не должно быть никаких накладных расходов, потому что вызов функции может отправляться статически (во время компиляции). В вашем примере известно, что динамический тип bar равен D (больше ничего не может быть), поэтому смещение подобъекта виртуального базового класса A может быть вычислено во время компиляции.

12 голосов
/ 05 апреля 2011

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

class A { ... };
class B : public A { ... }

Структура памяти B выглядит примерно так:

| B's stuff | A's stuff |

В этом случае компилятор знает, где находится A.Однако теперь рассмотрим случай MVI.

class A { ... };
class B : public virtual A { ... };
class C : public virtual A { ... };
class D : public C, public B { ... };

Макет памяти B:

| B's stuff | A's stuff |

Макет памяти C:

| C's stuff | A's stuff |

Но подождите!Когда создается экземпляр D, это не выглядит так.

| D's stuff | B's stuff | C's stuff | A's stuff |

Теперь, если у вас есть B *, если он действительно указывает на B, то A находится рядом с B-, но еслион указывает на D, тогда для получения A * вам действительно нужно пропустить субобъект C, и поскольку любой заданный B* может динамически указывать на B или D во время выполнения, вам потребуетсядинамически изменять указатель.Это, как минимум, означает, что вам придется создавать код, чтобы каким-то образом найти это значение, в отличие от того, чтобы значение заполнялось во время компиляции, что и происходит при одиночном наследовании.

7 голосов
/ 05 апреля 2011

По крайней мере в типичной реализации виртуальное наследование несет (небольшое!) Наказание за (по крайней мере, некоторые) доступ к элементам данных.В частности, вы обычно получаете дополнительный уровень косвенности для доступа к элементам данных объекта, из которого вы фактически получили.Это происходит потому, что (по крайней мере, в обычном случае) два или более отдельных производных классов имеют не только один и тот же базовый класс, но и один и тот же базовый класс object .Для этого оба производных класса имеют указатели с одинаковым смещением в наиболее производный объект и получают доступ к этим членам данных через этот указатель.

Хотя это технически не из-за виртуального наследования, вероятно, стоит отметить, что существует отдельное (опять же, небольшое) наказание за множественное наследование в целом.В типичной реализации одиночного наследования у вас есть указатель vtable с некоторым фиксированным смещением в объекте (довольно часто в самом начале).В случае множественного наследования вы, очевидно, не можете иметь два указателя vtable с одинаковым смещением, поэтому в итоге вы получите несколько указателей vtable, каждый с отдельным смещением в объекте.

IOW,Указатель vtable с единичным наследованием обычно равен static_cast<vtable_ptr_t>(object_address), но с множественным наследованием вы получаете static_cast<vtable_ptr_t>(object_address+offset).

Технически эти два понятия полностью разделены - но, конечно, почти единственное использование виртуального наследования - в сочетаниис множественным наследованием, так что оно в любом случае полуактуально.

2 голосов
/ 05 апреля 2011

Конкретно в Microsoft Visual C ++ существует реальная разница в размерах указателя на элемент.См. # pragma pointers_to_members .Как видно из этого листинга, наиболее общий метод - это «виртуальное наследование», которое отличается от множественного наследования, которое, в свою очередь, отличается от одиночного наследования.

Это означает, что для разрешения указателя требуется больше информации.to-member в случае наличия виртуального наследования, и это будет влиять на производительность, если только через количество данных, занятое в кеше ЦП - хотя, вероятно, также из-за длины поиска члена или количества переходовнеобходимо.

2 голосов
/ 05 апреля 2011

Я думаю, за виртуальное наследование нет штрафа за время выполнения. Не путайте виртуальное наследование с виртуальными функциями. Это две разные вещи.

виртуальное наследование гарантирует, что у вас есть только один подобъект A в случаях D. Так что я не думаю, что будет штраф за время выполнения один .

Однако могут возникать случаи, когда этот подобъект не может быть известен во время компиляции, поэтому в таких случаях за виртуальное наследование может быть наложен штраф. Один такой случай описан Джеймсом в его ответе.

1 голос
/ 16 февраля 2018

Ваш вопрос в основном сосредоточен на вызове обычных функций виртуальной базы, а не (гораздо) более интересного случая виртуальных функций виртуального базового класса (класса А в вашем пример) - но да, может быть стоимость. Конечно, все зависит от компилятора.

Когда компилятор скомпилировал A :: foo, он предположил, что «this» указывает на начало того, где члены данных для A находятся в памяти. В настоящее время компилятор может не знать, что класс A будет виртуальной базой любого другого класса. Но он с радостью генерирует код.

Теперь, когда компилятор скомпилирует B, изменений не будет, потому что, хотя A является виртуальным базовым классом, это все же единичное наследование, и в типичном случае компилятор разметит класс B, поместив данные класса A члены, за которыми сразу следуют члены данных класса B, так что B * может быть немедленно преобразован в A * без каких-либо изменений в значении, и, следовательно, никаких изменений не требуется. Компилятор может вызвать A :: foo, используя тот же указатель "this" (даже если он имеет тип B *), и это не причиняет вреда.

Та же самая ситуация для класса C - его все еще единственное наследование, и типичный компилятор поместит элементы данных A сразу после элементов данных C, так что C * может быть немедленно приведен к A * без каких-либо изменений в значении. Таким образом, компилятор может просто вызывать A :: foo с тем же указателем «this» (даже если он имеет тип C *), и в этом нет никакого вреда.

Однако ситуация совершенно иная для класса D. Макет класса D, как правило, будет состоять из элементов данных класса A, за которыми следуют элементы данных класса B, за которыми следуют элементы данных класса C, а затем элементы данных класса D.

Используя типичный макет, D * может быть немедленно преобразован в A *, поэтому штраф за A :: foo не налагается - компилятор может вызвать ту же самую процедуру, сгенерированную для A :: foo, без каких-либо изменений "это" и все в порядке.

Однако ситуация меняется, если компилятору необходимо вызвать функцию-член, такую ​​как C :: other_member_func, даже если C :: other_member_func не является виртуальным. Причина в том, что когда компилятор написал код для C :: other_member_func, он предположил, что компоновка данных, на которую ссылается указатель «this», является элементами данных A, за которыми сразу же следуют члены данных C. Но это не так для экземпляра D. Компилятору может потребоваться переписать и создать (не виртуальный) D :: other_member_func, просто чтобы позаботиться о разнице в разметке памяти экземпляра класса.

Обратите внимание, что это другая, но похожая ситуация при использовании множественного наследования, но при множественном наследовании без виртуальных баз компилятор может позаботиться обо всем, просто добавив смещение или исправление к указателю "this", чтобы учесть, где Базовый класс «встроен» в экземпляр производного класса. Но с виртуальными базами иногда требуется переписать функцию. Все зависит от того, к каким элементам данных обращается вызываемая (даже не виртуальная) функция-член.

Например, если класс C определил не виртуальную функцию-член C :: some_member_func, компилятору может потребоваться написать:

  1. C :: some_member_func при вызове из фактического экземпляра C (а не D), как определено во время компиляции (поскольку some_member_func не является виртуальной функцией)
  2. C :: some_member_func, когда та же функция-член вызывается из фактического экземпляра класса D, определенного во время компиляции. (Технически это подпрограмма D :: some_member_func. Несмотря на то, что определение этой функции-члена неявно и идентично исходному коду C :: some_member_func, сгенерированный объектный код может немного отличаться.)

если код для C :: some_member_func использует переменные-члены, определенные как в классе A, так и в классе C.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...