GCC如何在内存中存储成员函数

How does GCC store member functions in memory?

本文关键字:存储 成员 函数 内存 GCC      更新时间:2023-10-16

我试图最小化我的类在内存中占用的大小(数据和指令)。我知道如何最小化数据大小,但我不太熟悉GCC如何放置成员函数。

是否按照在类中声明的顺序存储在内存中?

为了在内存中表示数据,c++ class可以有普通成员函数或静态成员函数,也可以有virtual成员函数(包括一些virtual析构函数,如果有的话)。

普通或静态成员函数在数据内存中不占用任何空间,但是它们编译的代码当然会占用一些资源,例如,作为可执行文件或进程的文本或代码段中的二进制代码。当然,它们也可以需要static数据(或线程本地数据),或调用堆栈上的本地数据(例如本地变量)。

我的答案是面向Linux的。我不懂Windows,也不知道GCC是怎么工作的

虚成员函数通常通过虚方法表(或vtable)实现;具有一些虚成员函数的class通常具有单个(假设单继承)虚表指针的实例,该虚表指针指向该虚表(实际上是打包在文本段中的一些数据)。

注意,变量不是强制性的,c++ 11标准也不要求它。

当您使用多重继承时,事情变得更加复杂,对象可能具有多个虚函数表指针。

因此,如果您有一个class(根类或使用单继承),则虚拟成员函数的消耗是每个实例一个虚值表指针(加上单个虚值表本身所需的小空间)。如果你只有一个虚成员函数(或析构函数)或一千个这样的虚成员函数(改变的是虚函数表本身),它不会改变(对于每个实例)。每个类都有自己的单个虚值表(除非它没有虚成员函数),并且每个实例通常有一个虚值表指针(对于单继承情况)。

GCC编译器可以自由地组织虚值表(它的顺序和布局是你不应该关心的实现细节);再看看这个。在大多数最新的GCC版本中(对于单继承),虚函数表指针是对象的第一个字,虚函数表按照虚方法声明的顺序包含函数指针,但您不应该依赖于这些细节。

GCC编译器可以自由地组织代码段中的函数,它实际上会重新排序它们(例如为了优化)。上次我看的时候,它们是倒序排列的。但是你当然不应该依赖这个顺序!顺便说一句,GCC可以在优化时内联函数(即使没有标记为inline)和克隆函数。你也可以编译和链接与链接时优化(例如make CXX='g++ -flto -Os'),你可以要求配置文件引导的优化(对于GCC: -fprofile-generate, -fprofile-use, -fauto-profile等…)

你不应该依赖于编译器(和链接器)如何组织函数代码或虚表将优化留给编译器(这种优化取决于您的目标机器、编译器标志和编译器版本)。你也可以使用函数属性给GCC(或Clang/LLVM)编译器(例如__attribute__((cold)), __attribute__((noinline))等....)提示

如果你真的需要知道函数是如何放置的(我认为这是非常错误的),研究生成的汇编代码(例如使用g++ -O -fverbose-asm -S),并意识到它可能因编译器版本而异!

如果您需要在Linux和Posix系统上在运行时从函数的名称中查找函数的地址,请考虑使用dlsym(对于Linux,请参阅dlsym(3),它也记录了dladdr)。注意名称混淆,可以通过声明extern "C"这样的函数来禁用它(参见c++ dlopen minihowto)。

BTW,你可以编译和链接-rdynamic(这是非常有用的dlopen等…)。如果你真的需要知道函数的地址,使用nm(1)作为nm -C your-executable

您也可以阅读针对目标平台(和编译器)的ABI规范和调用约定,例如Linux x86-64 ABI规范。

假设我们有一个类型T,它有4个实例方法。

class T {
    public:
        void member_function_1() { ... }
        void member_function_2() { ... }
        void member_function_3() { ... }
        void member_function_4() { ... }
};
实例化1个T副本和实例化100万个T副本时,这些方法占用的内存量是一样的。