Как GCC хранит функции-члены в памяти?
Я пытаюсь минимизировать размер, который мой класс занимает в памяти (как данные, так и инструкции). Я знаю, как минимизировать размер данных, но я не слишком знаком с тем, как GCC размещает функции-члены.
Они хранятся в памяти, в том же порядке, в котором они объявлены в классе?
2 ответа
Для представления данных в памяти, C++ class
может иметь как простые, так и статические функции-члены, или virtual
функции-члены (включая некоторые virtual
деструктор, если есть).
Простые или статические функции-члены не занимают места в памяти данных, но, разумеется, их скомпилированный код потребляет некоторый ресурс, например, в виде двоичного кода в текстовом или кодовом сегменте вашего исполняемого файла или вашего процесса. Конечно, они также могут потребовать static
данные (или локальные данные потока) или локальные данные (например, локальные переменные) в стеке вызовов.
Мой ответ ориентирован на Linux. Я не знаю Windows и не знаю, как работает GCC.
Виртуальные функции-члены очень часто реализуются через таблицу виртуальных методов (или vtable); class
некоторые виртуальные функции-члены обычно имеют экземпляры с одним (при условии единственного наследования) указателем vtable, указывающим на этот vtable (который представляет собой практически некоторые данные, упакованные в текстовый сегмент).
Обратите внимание, что таблицы vtables не являются обязательными и не требуются стандартом C++11. Но я не знаю никакой реализации C++, не использующей их.
Когда вы используете множественное наследование, вещи становятся более сложными, объекты могут иметь несколько указателей vtable.
Так что если у вас есть class
(либо корневым классом, либо с использованием одиночного наследования), потребление виртуальных функций-членов составляет один указатель виртуальной таблицы на экземпляр (плюс небольшое пространство, необходимое для самой виртуальной таблицы). Он не изменится (для каждого экземпляра), если у вас есть только одна виртуальная функция-член (или деструктор) или тысяча из них (что изменится, так это сам vtable). Каждый класс имеет свой собственный отдельный vtable (если только он не имеет виртуальной функции-члена), и каждый экземпляр обычно имеет один (для случая с одним наследованием) указатель vtable.
Компилятор GCC может свободно организовать vtable по своему усмотрению (а его порядок и расположение - это детали реализации, которые вам не нужны); Смотрите также это. На практике (для одиночного наследования) для самых последних версий GCC указатель vtable является первым словом объекта, а vtable содержит указатели функций в порядке объявления виртуального метода, но вы не должны зависеть от таких деталей.
Компилятор GCC может свободно организовывать функции в сегменте кода по своему усмотрению, и он фактически переупорядочивает их (например, для оптимизации). В прошлый раз я посмотрел, он заказал их в обратном порядке. Но вы точно не должны зависеть от этого порядка! Кстати, GCC может встроенные функции (даже если не помечены inline
) и функции клонирования при оптимизации. Вы также можете скомпилировать и связать с оптимизацией времени соединения (например, make CXX='g++ -flto -Os'
), и вы можете запросить оптимизацию по профилю (для GCC: -fprofile-generate
, -fprofile-use
, -fauto-profile
так далее...)
Вы не должны зависеть от того, как компилятор (и компоновщик) организует код функции или vtables. Оставьте оптимизации для компилятора (и такая оптимизация зависит от вашей целевой машины, флагов вашего компилятора и версии компилятора). Вы также можете использовать атрибуты функции, чтобы давать подсказки компилятору GCC (или Clang/LLVM) (например, __attribute__((cold))
, __attribute__((noinline))
и тд и тп....)
Если вам действительно нужно знать, как размещаются функции (что, IMHO, очень неправильно), изучите сгенерированный код сборки (например, используя g++ -O -fverbose-asm -S
) и помните, что это может варьироваться в зависимости от версии компилятора!
Если вам нужно в системах Linux и Posix во время выполнения узнать адрес функции по ее имени, рассмотрите возможность использования dlsym (для Linux см. Dlsym (3), которая также документирует dladdr
). Помните об искажении имен, которое можно отключить, объявив такие функции как extern "C"
(см. C++ dlopen minihowto).
Кстати, вы можете скомпилировать и связать с -rdynamic
(что очень полезно для dlopen
так далее...). Если вам действительно нужно знать адрес функций, используйте nm(1) как nm -C
ваш исполняемый файл
Вы также можете прочитать спецификацию ABI и соглашения о вызовах для вашей целевой платформы (и компилятора), например, спецификацию ABI для Linux x86-64.
Допустим, у нас есть тип T
с 4 методами экземпляра.
class T {
public:
void member_function_1() { ... }
void member_function_2() { ... }
void member_function_3() { ... }
void member_function_4() { ... }
};
Объем памяти, занимаемый этими методами, будет одинаковым, если мы создадим 1 копию T
или если мы создадим 1 миллион копий T
,