Какова стоимость наследования? - PullRequest
7 голосов
/ 27 августа 2011

Это довольно простой вопрос, но я все еще не уверен:

Если у меня есть класс, который будет создаваться миллионы раз - желательно ли не выводить его из какого-то другого класса? Другими словами, несет ли наследование некоторую стоимость (с точки зрения памяти или времени выполнения для создания или уничтожения объекта), о которой я должен беспокоиться на практике?

Пример:

class Foo : public FooBase { // should I avoid deriving from FooBase?
 // ...
};

int main() {
  // constructs millions of Foo objects...
}

Ответы [ 7 ]

20 голосов
/ 27 августа 2011

Наследование от класса ничего не стоит во время выполнения.

Экземпляры класса, конечно, будут занимать больше памяти, если у вас есть переменные в базовом классе, но не больше, чем если бы они были непосредственно в производном классе, и вы ничего не наследовали.

Это не учитывает virtual методы, которые требуют небольших затрат времени выполнения.

tl; dr: Вам не стоит об этом беспокоиться.

5 голосов
/ 27 августа 2011

Я немного удивлен некоторыми ответами / комментариями ...

наследование несет некоторую стоимость (с точки зрения памяти)

Да. Дано:

namespace MON {
class FooBase {
public:
    FooBase();
    virtual ~FooBase();
    virtual void f();
private:
    uint8_t a;
};

class Foo : public FooBase {
public:
    Foo();
    virtual ~Foo();
    virtual void f();
private:
    uint8_t b;
};

class MiniFoo {
public:
    MiniFoo();
    ~MiniFoo();
    void f();
private:
    uint8_t a;
    uint8_t b;
};

    class MiniVFoo {
    public:
        MiniVFoo();
        virtual ~MiniVFoo();
        void f();
    private:
        uint8_t a;
        uint8_t b;
    };

} // << MON

extern "C" {
struct CFoo {
    uint8_t a;
    uint8_t b;
};
}

в моей системе размеры следующие:

32 bit: 
    FooBase: 8
    Foo: 8
    MiniFoo: 2
    MiniVFoo: 8
    CFoo: 2

64 bit:
    FooBase: 16
    Foo: 16
    MiniFoo: 2
    MiniVFoo: 16
    CFoo: 2

время выполнения для строительства или уничтожения объекта

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

весь предмет гораздо сложнее, но это даст вам представление о затратах.

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

относительно производительности процессора, я создал простой тест, который создал миллионы этих типов в стеке и в куче и назвал f, результаты:

FooBase 16.9%
Foo 16.8%
Foo2 16.6%
MiniVFoo 16.6%
MiniFoo 16.2%
CFoo 15.9%

примечание: Foo2 происходит от foo

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

Обрезка функций выделения и вектора создания / уничтожения вектора из выборки дает следующие числа:

Foo 19.7%
FooBase 18.7%
Foo2 19.4%
MiniVFoo 19.3%
MiniFoo 13.4%
CFoo 8.5%

, что означает, что виртуальные варианты занимают в два раза больше времени, чем CFoo для выполнения своих конструкторов, деструкторов и вызовов, а MiniFoo примерно в 1,5 раза быстрее.

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

Create many MiniFoos as array 0.2%
Create many CFoos as array 0.1%

Также имейте в виду, что размеры MiniFoo и CFoo занимают 1/4 - 1/8 памяти на элемент, а непрерывное распределение устраняет необходимость хранить указатели на динамические объекты. Затем вы могли бы отслеживать объект в реализации несколькими способами (указатель или индекс), но массив также может значительно снизить требования по распределению на клиентах (uint32_t против указателя на 64-битную арку) - плюс вся бухгалтерская отчетность, требуемая системой для распределений (что важно при работе с таким большим количеством небольших распределений).

В частности, размеры в этом тесте потребляются:

32 bit
    267MB for dynamic allocations (worst)
    19MB for the contiguous allocations
64 bit
    381MB for dynamic allocations (worst)
    19MB for the contiguous allocations

это означает, что требуемая память была уменьшена более чем на десять, а время, потраченное на выделение / освобождение, значительно лучше!

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

На практике динамические типы имеют тенденцию экспортировать больше символов (методов, dtors, vtables), что может заметно увеличить размер двоичного файла.

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

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

Я думаю, что все мы, ребята, программировали слишком много, как одинокий волк. Мы забываем взять стоимость обслуживания + читаемость + расширения в отношении функций.Вот мое взятие

Стоимость наследования ++

  1. Для небольших проектов: время на разработку увеличивается.Легко написать весь глобальный код судоку.Мне всегда требовалось больше времени, чтобы написать наследование классов, чтобы делать правильные вещи.
  2. В небольших проектах: время на изменение увеличивается.Не всегда легко изменить существующий код для подтверждения существующего интерфейса.
  3. Время на разработку увеличивается.
  4. Программа немного неэффективна из-за многократной передачи сообщений, а не из-за незащищенности (я имею в виду)члены-члены.:))
  5. Только для вызовов виртуальных функций через указатель на базовый класс возможна одна дополнительная разыменование.
  6. Небольшое штрафное место в терминах RTTI
  7. Для полноты картины добавлю, что слишком много классов добавят слишком много типов, и это неизбежно увеличит время компиляции, независимо от того, насколько оно маленьким.
  8. Существует также стоимость отслеживания нескольких объектов с точки зрения объекта базового класса и всего для системы времени выполнения, что, очевидно, означает небольшое увеличение размера кода + небольшое снижение производительности во время выполнения из-за механизма делегирования исключений (будь тоиспользуете ли вы его или нет).
  9. Вам не нужно неестественно крутить руку, используя PIMPL, если все, что вы хотите сделать, это изолировать пользователей ваших функций интерфейса от перекомпиляции.(Это БОЛЬШАЯ стоимость, поверьте мне.)

Стоимость наследования -

  1. Поскольку размер программы увеличивается более чем на 1/2 тысячи строк, она становится более удобной для обслуживанияс наследством.Если вы программируете только один раз, вы можете легко перенести код без объекта до 4k / 5k строк.
  2. Снижается стоимость исправления ошибок.
  3. Вы можете легко расширить существующую среду для более сложных задач.

Я знаю, что я маленький адвокат дьяволов, но я думаю, что мы должны быть справедливыми.

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

Во многом это зависит от реализации.Но есть некоторые общие черты.

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

И любой вызов виртуальной функции будет включать скрытый уровень косвенности - вместо перехода к адресу функции, который был разрешен по ссылкевремя вызова будет включать в себя чтение адреса из vtable, а затем переход к нему.

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

OTOH, вы сказали, что вы будете создавать и уничтожать миллионы этих объектов.В большинстве случаев самая большая стоимость - это не создание объекта, а выделение памяти для него.

Итак, вы могли бы выиграть от использования собственных пользовательских распределителей памяти для класса.

http://www.cprogramming.com/tutorial/operator_new.html

2 голосов
/ 27 августа 2011

Дело в том, что если вы сомневаетесь в том, следует ли вам наследовать или нет, ответ заключается в том, что вы не должны.Наследование является вторым наиболее взаимосвязанным отношением в языке.

Что касается разницы в производительности, то в большинстве случаев разницы почти не должно быть, если только вы не начнете использовать множественное наследование, где, если одна из баз имеет виртуальные функции,отправка будет иметь дополнительную (минимальную, незначительную) стоимость, если базовый подобъект не выровнен с окончательным переопределением, поскольку компилятор добавит thunk для настройки указателя this.

2 голосов
/ 27 августа 2011

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

Будьте осторожны, во многих случаях лучше составлять (иметь член данных класса base, а не наследовать его), особенно если вы не переопределяете виртуальные функции и ваши отношения между производным 'и' база 'не является "своего рода" отношениями. Но с точки зрения использования процессора и памяти оба эти метода эквивалентны.

2 голосов
/ 27 августа 2011

Если вам нужна функциональность FooBase в Foo, вы можете извлечь или использовать композицию.Деривация имеет стоимость vtable, а FooBase имеет стоимость указателя на FooBase, FooBase и voo FooBase.Таким образом, они (примерно) похожи, и вам не нужно беспокоиться о стоимости наследования.

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