В C ++, где в памяти помещены функции класса? - PullRequest
18 голосов
/ 16 марта 2009

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

Это верно?

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

Как-то влияют на это виртуальные функции, полиморфизм, фактор наследования?

А как насчет объектов из динамически связанных библиотек? Я предполагаю, что DLL получают свой собственный стек, кучу, код и сегменты данных.

Простой пример (не может быть синтаксически правильным):

// parent class
class Bar
{
public:
    Bar()  {};
    ~Bar() {};

    // pure virtual function
    virtual void doSomething() = 0;

protected:
    // a protected variable
    int mProtectedVar;
}

// our object class that we'll create multiple instances of
class Foo : public Bar
{
public:
    Foo()  {};
    ~Foo() {};

    // implement pure virtual function
    void doSomething()          { mPrivate = 0; }

    // a couple public functions
    int getPrivateVar()         { return mPrivate; }
    void setPrivateVar(int v)   { mPrivate = v; }

    // a couple public variables
    int mPublicVar;
    char mPublicVar2;

private:
    // a couple private variables
    int mPrivate;
    char mPrivateVar2;        
}

О том, сколько памяти должны занимать 100 динамически распределенных объектов типа Foo, включая место для кода и всех переменных?

Ответы [ 9 ]

27 голосов
/ 16 марта 2009

Не обязательно, что «каждому объекту - при создании - в HEAP будет предоставлено место для переменных-членов». Каждый создаваемый вами объект будет занимать некоторое ненулевое пространство где-то для его переменных-членов, но где находится вопрос о том, как вы выделяете сам объект. Если объект имеет автоматическое (стековое) размещение, то же самое относится и к его элементам данных. Если объект размещен в свободном хранилище (куче), то также будут и его элементы данных. В конце концов, каково распределение объекта, отличного от распределения его элементов данных?

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

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

Что касается функций-членов, то код для них, скорее всего, не отличается от кода свободных функций с точки зрения того, куда он идет в исполняемом образе. В конце концов, функция-член в основном является свободной функцией с неявным указателем «this» в качестве первого аргумента.

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

Я не уверен, что вы имеете в виду, когда библиотеки DLL получают свой собственный стек. DLL не является программой, и ей не нужно иметь стек (или кучу), поскольку объекты, которые она размещает, всегда размещаются в контексте программы, которая имеет свой собственный стек и кучу. То, что в DLL есть сегменты кода (текста) и данных, имеет смысл, хотя я не являюсь экспертом в реализации таких вещей в Windows (которую я предполагаю, что вы используете, учитывая вашу терминологию).

6 голосов
/ 16 марта 2009

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

Тогда это становится несколько сложным. Некоторые из проблем ...

  • Компилятор может, если он хочет или получает инструкции, встроенный код. Таким образом, даже если это может быть простая функция, если она используется во многих местах и ​​выбрана для встраивания, может быть сгенерировано много кода (распределенного по всему коду программы).
  • Виртуальное наследование увеличивает полиморфный размер каждого члена. VTABLE (виртуальная таблица) скрывается вместе с каждым экземпляром класса, используя виртуальный метод, содержащий информацию для диспетчеризации во время выполнения. Эта таблица может стать довольно большой, если у вас много виртуальных функций или множественное (виртуальное) наследование. Пояснение: VTABLE для каждого класса, но указатели на VTABLE хранятся в каждом экземпляре (в зависимости от структуры типа объекта).
  • Шаблоны могут привести к раздуванию кода. Каждое использование шаблонного класса с новым набором параметров шаблона может генерировать новый код для каждого члена. Современные компиляторы стараются свернуть это как можно больше, но это сложно.
  • Выравнивание / заполнение структуры может привести к тому, что простые экземпляры класса будут больше, чем вы ожидаете, поскольку компилятор дополняет структуру целевой архитектуры.

При программировании используйте оператор sizeof для определения размера объекта, а не жесткий код. Используйте приблизительную метрику «Сумма размера переменной члена + некоторый VTABLE (если он существует)» при оценке того, насколько дорогими будут большие группы экземпляров, и не беспокойтесь слишком сильно о размере кода. Оптимизируйте позже, и если какие-то неочевидные проблемы снова станут что-то значить, я буду весьма удивлен.

2 голосов
/ 16 марта 2009

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

Это верно для статически и динамически связанного кода. Фактический код все живет в «текстовой» области. Большинство операционных систем на самом деле могут делить код DLL между несколькими приложениями, поэтому, если несколько приложений используют одни и те же библиотеки DLL, только одна копия находится в памяти, и оба приложения могут использовать ее. Очевидно, что нет никакой дополнительной экономии от разделяемой памяти, если только одно приложение использует связанный код.

1 голос
/ 03 ноября 2009

Информация, приведенная выше, очень помогает и дает мне некоторое представление о структуре памяти C ++. Но я хотел бы добавить, что независимо от того, сколько виртуальных функций в классе, всегда будет только 1 VPTR и 1 VTABLE на класс. После того как все VPTR указывают на VTABLE, нет необходимости использовать более одного VPTR в случае нескольких виртуальных функций.

1 голос
/ 16 марта 2009

если скомпилировано как 32-битное. тогда sizeof (Bar) должен дать 4. Foo должен добавить 10 байтов (2 дюйма + 2 символа).

Так как Foo наследуется от Bar. Это как минимум 4 + 10 байтов = 14 байтов.

GCC имеет атрибуты для упаковки структур, поэтому нет заполнения. В этом случае 100 записей заняли бы 1400 байт + небольшие накладные расходы для выравнивания распределения + некоторые накладные расходы для управления памятью.

Если упакованный атрибут не указан, это зависит от выравнивания компиляторов.

Но это не учитывает, сколько занимает vtable памяти и размер скомпилированного кода.

1 голос
/ 16 марта 2009

Вы не можете точно сказать, сколько памяти класс или объекты X будут занимать в ОЗУ.

Однако, чтобы ответить на ваши вопросы, вы правы, что код существует только в одном месте, он никогда не «выделяется». Таким образом, код предназначен для каждого класса и существует независимо от того, создаете вы объекты или нет. Размер кода определяется вашим компилятором, и даже тогда компиляторам часто говорят оптимизировать размер кода, что приводит к различным результатам.

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

Что касается библиотек DLL и других библиотек ... правила не отличаются в зависимости от того, откуда пришел код, поэтому это не является фактором использования памяти.

0 голосов
/ 16 марта 2009

Очень сложно дать точный ответ на ваш вопрос, так как это зависит от реализации, но приблизительные значения для 32-битной реализации могут быть:

int Bar::mProtectedVar;    // 4 bytes
int Foo::mPublicVar;        // 4 bytes
char Foo::mPublicVar2;     // 1 byte

Здесь есть проблемы со ссылками, и итоговая сумма может быть 12 байт. У вас также будет vptr - скажем, anoter 4 байта. Таким образом, общий размер данных составляет около 16 байт на экземпляр. Невозможно сказать, сколько места займет код, но вы правы, считая, что существует только одна копия кода, совместно используемая всеми экземплярами.

Когда вы спрашиваете

Полагаю, DLL получают свой собственный стек, сегменты кучи, кода и данных.

Ответ заключается в том, что на самом деле нет большой разницы между данными в DLL и данными в приложении - в основном они разделяют все между собой. Так должно быть, если подумать - если у них были разные стеки например) как могут работать вызовы функций?

0 голосов
/ 16 марта 2009

Да, верно, код не дублируется при создании экземпляра объекта. Что касается виртуальных функций, то правильный вызов функции определяется с помощью vtable , но это само по себе не влияет на создание объекта.

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

0 голосов
/ 16 марта 2009

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

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

Как и в c, вы можете использовать оператор sizeof (classname / datatype) для получения размера в байтах класса.

...