gpt4 book ai didi

c++ - G++无法内联多态方法吗?

转载 作者:行者123 更新时间:2023-12-02 11:22:06 29 4
gpt4 key购买 nike

看来具有优化功能的G++无法内联来自转换单元静态变量的琐碎函数。下面的代码和编译输出示例。注意,函数 can_inline_local 通过使用DerivedType的本地实例完美地内联了调用,但是 cant_inline_static 的调用时间更长。

在您对我进行过早的优化报警之前,我想为自己辩护,说多态继承将非常清楚地描述我的内核级串行驱动程序中断服务例程。而且如果G++只能为我内联虚拟调用(使用我在编译时应该知道的信息),那么我将拥有可编译为C性能的清晰可测试的代码。

我正在使用arm-none-eabi-g++ -v
gcc版本4.9.3 20150529(预发行)(15:4.9.3 + svn227297-1)

arm-none-eabi-g++ -std = gnu ++ 11 -O3 -c -o inline.o inline.cpp && arm-none-eabi-objdump inline.o -S> inline.dump

inline.cpp:

extern "C"{
int * const MEMORY_MAPPED_IO_A = (int*)0x40001000;
int * const MEMORY_MAPPED_IO_B = (int*)0x40002000;
}

namespace{
/** Anon namespace should make these
typedefs static to this translation unit */
struct BaseType{
void* data;
virtual void VirtualMethod(int parameter){
*MEMORY_MAPPED_IO_A = parameter;
}

void VirtualCaller(int parameter){
this->VirtualMethod(parameter);
}
};

struct DerivedType : BaseType{
void VirtualMethod(int parameter) final {
*MEMORY_MAPPED_IO_B = parameter;
}
};

/** static keyword here may be superfluous */
static BaseType basetype;
static DerivedType derivedtype;

extern "C"{
void cant_inline_static(int parameter){
derivedtype.VirtualCaller(1);
}

void can_inline_local(int parameter){
DerivedType localobj;
localobj.VirtualCaller(1);
}
}
}

inline.dump
inline.o:     file format elf32-littlearm


Disassembly of section .text:

00000000 <_ZN12_GLOBAL__N_18BaseType13VirtualMethodEi>:
0: e59f3004 ldr r3, [pc, #4] ; c <_ZN12_GLOBAL__N_18BaseType13VirtualMethodEi+0xc>
4: e5831000 str r1, [r3]
8: e12fff1e bx lr
c: 40001000 .word 0x40001000

00000010 <_ZN12_GLOBAL__N_111DerivedType13VirtualMethodEi>:
10: e59f3004 ldr r3, [pc, #4] ; 1c <_ZN12_GLOBAL__N_111DerivedType13VirtualMethodEi+0xc>
14: e5831000 str r1, [r3]
18: e12fff1e bx lr
1c: 40002000 .word 0x40002000

00000020 <cant_inline_static>:
20: e59f0028 ldr r0, [pc, #40] ; 50 <cant_inline_static+0x30>
24: e5903000 ldr r3, [r0]
28: e59f2024 ldr r2, [pc, #36] ; 54 <cant_inline_static+0x34>
2c: e5933000 ldr r3, [r3]
30: e1530002 cmp r3, r2
34: 1a000003 bne 48 <cant_inline_static+0x28>
38: e3a02001 mov r2, #1
3c: e59f3014 ldr r3, [pc, #20] ; 58 <cant_inline_static+0x38>
40: e5832000 str r2, [r3]
44: e12fff1e bx lr
48: e3a01001 mov r1, #1
4c: e12fff13 bx r3
...
58: 40002000 .word 0x40002000

0000005c <can_inline_local>:
5c: e3a02001 mov r2, #1
60: e59f3004 ldr r3, [pc, #4] ; 6c <can_inline_local+0x10>
64: e5832000 str r2, [r3]
68: e12fff1e bx lr
6c: 40002000 .word 0x40002000

Disassembly of section .text.startup:

00000000 <_GLOBAL__sub_I_cant_inline_static>:
0: e59f3014 ldr r3, [pc, #20] ; 1c <_GLOBAL__sub_I_cant_inline_static+0x1c>
4: e59f2014 ldr r2, [pc, #20] ; 20 <_GLOBAL__sub_I_cant_inline_static+0x20>
8: e2831008 add r1, r3, #8
c: e2833018 add r3, r3, #24
10: e5821008 str r1, [r2, #8]
14: e5823000 str r3, [r2]
18: e12fff1e bx lr
...

更新

只需注释掉 void *数据; BaseType中的字段允许主动优化琐碎的虚拟调用。以下是objdump。如果该类具有可能未初始化的数据成员,则G++似乎不信任使用静态实例方法。我有什么办法可以指定一个类就是它的外观,并且不需要构造或初始化?如果编译器假设发生这种情况,由于我不知道的某些过度设计/深奥的功能,所有C++都会失效吗?我觉得我正在捕获稻草,但这又是一个值得问的问题。
inline.o:     file format elf32-littlearm


Disassembly of section .text.cant_inline_static:

00000000 <cant_inline_static>:
0: 2201 movs r2, #1
2: 4b01 ldr r3, [pc, #4] ; (8 <cant_inline_static+0x8>)
4: 601a str r2, [r3, #0]
6: 4770 bx lr
8: 40002000 .word 0x40002000

Disassembly of section .text.can_inline_local:

00000000 <can_inline_local>:
0: 2201 movs r2, #1
2: 4b01 ldr r3, [pc, #4] ; (8 <cant_inline_static+0x8>)
4: 601a str r2, [r3, #0]
6: 4770 bx lr
8: 40002000 .word 0x40002000

最终更新

我已经解决了cant_inline_static开头出现的簿记代码。它只是采用静态实例的派生类型,取消引用其vtable,查找VirtualMethod条目,然后将其与DerivedType::VirtualMethod的.text地址进行比较。如果它们匹配:内联过程将运行。如果它们不同:调用实例的vtable方法。

似乎G++期望虚拟调用最终是DerivedType::VirtualMethod,但担心静态DerivedType派生类型变量的vtable可能指向其他方法。如果您初始化DerivedType的所有成员(和继承的成员)变量,则G++会获得完全内联'VirtualMethod'所需的置信度。正如@rici解释的那样,很可能与'derivedtype'实例放置了.data(显式初始化)而不是.bss有关。

要添加的有趣一点:如果派生类型实例和基类型实例都调用VirtualCaller,则G++会添加簿记代码,而与成员初始化无关。

在这一点上,我通过发现某些家伙是如何编写G++优化器的这一部分来扮演考古学家的角色。这是一个有趣的旅程。我在这里有一些非常好的帮助。而且我在此过程中学到了很多有关虚拟方法性能的知识。

最佳答案

TL; DR :
void* data;替换void* data = 0;。 (如果有更多的数据成员,则必须将它们中的每一个初始化为某个编译时常数值。)
完成此操作后,g++将在目标文件中预初始化derivedtype,而不是在运行时进行初始化。

免责声明:
这不是语言律师的问题,所以我没有写语言律师的答案。以下大多数内容取决于实现,这意味着它可能不适用于与我尝试过的版本不同的任何特定编译器,版本或月相。它专门指的是GCC,尤其是ELF目标文件。它涵盖了英特尔和ARM体系结构,但我不主张对其进行概括。
C++中的静态初始化充满了(有时会说“被……困扰”)恶魔占据的细节和极端情况。下面的说明过于简单,因为(1)在这种情况下,大多数细节都无关紧要; (2)我不知道ELF加载程序的所有详细信息,尤其是在ARM平台上。但我认为它或多或少与现实相符。

静态初始化和C++标准:
就像我在上面说的那样,这不是语言律师的答案,因此我不会在标准中提供长引号。您可以阅读标准本身的§3.6.2([basic.start.init])。从本质上讲,如果初始化程序的行为良好且没有副作用,则编译器可以安排在想要的任何时候初始化全局变量,但不得迟于严格必要的时间。要明确后者,这是唯一的标准报价:

If the initialization is deferred to some point in time after the first statement of main, it shall occur before the first odr-use of any function or variable defined in the same translation unit as the variable to be initialized. (§3.6.2, para. 4).


允许延迟初始化的主要原因是允许动态加载。动态(或按需)加载允许程序在实际加载所有模块并将其链接到可执行文件之前开始运行。通过将可执行文件与读取程序所需的所有库中所需的慢速磁盘访问重叠,可以加快启动速度(例如,可执行文件可以立即绘制启动画面)。全部,取决于对程序的特定用户请求。
因此,该标准允许(但不要求)某种形式的“按需”初始化;为了实现这一点,它可以在“任何功能或变量的奇特使用”之前插入初始化检查,这可能是第一个此类使用。这就是您在(内联)调用 cant_inline_static之前看到的代码。
初始化和多态对象 derivedtype是多态类的实例非常重要。多态类的每个实例都有一个额外的隐藏数据成员,该成员包含一个指向函数指针(和其他信息) vector 的指针(“vptr”),通常称为“vtable”。这就是实现虚拟函数调用的方式:在运行时,通过对象的vtable间接调用虚拟函数。 [注1]关于这一点还有很多可以说的,但是这里的要点是,多态类的每个实例都有一个vptr,其值需要初始化。
因此,不是“不需要初始化对象”的情况。多态类的每个实例都需要初始化。但是,vtable的(符号)地址在编译时是已知的,因此可以将其作为常量初始化执行。编译器认为合适与否,是因为vtables和vptrs是实现细节,而不是C++标准规定的。 (这是一种礼貌的小说。我不相信有一个不使用vtables和vptrs的实现。vtable的确切布局和内容因实现而异。)
初始化和加载程序
在程序(翻译单元的集合)的编译(“翻译”)与 main()的执行开始之间,需要将各种翻译后的翻译单元(目标文件)读入内存并组合成程序镜像。在此过程中,需要为在一个翻译单元中定义并在另一翻译单元中使用的名称分配地址,并且需要在使用这些地址的位置插入这些地址。即使在一个翻译单元中,通常也需要修改对名称的引用,以考虑分配给该名称的实际地址。
这些不同的过程-加载,链接,重定位-并未由C++标准进行详细定义(或完全没有定义),该标准将程序的整个执行过程(包括上述步骤)视为程序执行的一部分。因此,某些描述为“在main的第一条语句之前”发生的实际发生在链接和加载步骤期间。
在Intel / ARM平台上,gcc将转换单元编译为 ELF目标文件。还有一个链接器,它将ELF对象文件组合成单个ELF可执行文件(可能带有对外部库的引用)。 ELF文件由多个“节”组成,每个节具有不同的特征。
ELF定义了大量的节类型和选项,但是实际上有三类主要节,它们通常且容易混淆地描述为文本,数据和bss。
  • 文本节表示只读存储器。 (该限制可能会或可能不会由操作系统强制执行)。这包括程序本身,还包括初始化为编译时常数值的静态常数对象。目标文件包含这些部分的实际位表示形式,以及在链接时在何处插入符号地址的某些指示。 [笔记2]
  • 数据节表示已初始化的读写存储器。其中包括静态对象,其静态值可以由编译器计算,但可以在运行时进行修改。同样,目标文件包含初始值的实际位表示。
  • bss节(名称是出于历史的好奇心,有关详细信息,请参见Wikipedia)代表零初始化的读写存储器。这用于静态对象,其静态对象的初始值将在运行时(必要时)进行计算。目标文件仅包含这些对象的大小。没有提供位表示。加载程序通过显式清除分配的内存或使用虚拟内存系统将内存映射到将在第一个引用上清零的页面,将这些节的初始值为零。

  • ELF还允许编译器提供初始化部分,该部分是要在加载过程结束时(即在实际的主要可执行文件开始之前)执行的可执行代码。 [注3]
    可以将其初始值为主要为零的读写对象放置在具有显式零的数据部分或bss部分以及用于运行时初始化非零元素的代码中。如果它在bss节中,则初始化代码可以在初始化节中,或者可以在延迟执行的构造函数中。 Gcc将根据自身的启发式和优化标志选择上述策略之一。
    我不知道gcc使用的所有试探法,但我相信它通常会更喜欢bss节,这是合乎逻辑的,因为在循环中对内存进行零初始化通常比从磁盘复制一堆零更快。文件,以及将字节保存在磁盘文件本身中。但是,如果您明确对数据进行零初始化,则gcc将使用数据段,除非整个对象都被零初始化(即使这样,如果您指定 -fno-zero-initialized-in-bss)。因此,您可以观察到以下两者之间的区别:
    struct S {
    int one = 1;
    int zeros[1000000] = {0};
    };
    S s;
    struct S {
    int one = 1;
    int zeros[1000000];
    };
    S s;
    在我的系统上,生成的目标文件的大小为4,000,962与2184字节。
    返回OP
    因此,在问题代码中,我们有一个静态对象 derivedtype,它带有一个(继承的)默认初始化的数据成员。由于它是一个多态对象的实例,因此它还具有一个内部vptr数据成员,需要对其进行初始化。因此,它看起来像一个混合数据对象,因此gcc将其放在bss节中,并在需要时插入代码以(延迟)初始化它。
    显式初始化数据成员(甚至初始化为0)会导致gcc将对象放在数据节中,从而使其静态初始化。这避免了懒惰的初始化代码。
    但实际上不需要初始化该对象
    碰巧的是,在这种特殊情况下,不可能通过指向 derivedtype的指针来调用虚拟成员函数。因此,从某种意义上说,如果vptr成员从未初始化过,那实际上并不重要。但是,期望编译器甚至考虑检查该场景是完全不合理的。如果创建多态类,则可能仅是因为您打算多态调用成员函数。为了确定是否可能发生多态调用而对该类的实例进行完全转义分析,几乎总是会浪费时间,因此没有理由要有人在编译器中包括该检查。 (这是个人观点。您可以不同意。:-))
    如果您确实要告诉编译器特定的成员函数调用不是多态的,则可以使用显式调用自由地这样做:
    derivedtype.DerivedType::VirtualMethod(p);
    走得更远,您可以使用类似以下内容的方法调用不使用 this的多态方法(即如果不是多态的则可能是 static)摆脱困境:
    ((DerivedType)nullptr)->DerivedType::VirtualMethod(p);
    甚至:
    ((decltype(derivedtype)*)(nullptr)->decltype(derivedtype)::VirtualMethod(p);
    但是在您的代码中,这将无效,因为您实际上调用了 VirtualCaller,后者显式使用了 this。 (说实话,我不太了解那里的逻辑)。但是,上面的hack(我在代码审查中不会接受)确实避免了使用odt的 derivedtype,从而避免了对其进行初始化。 See it here上的 Godbolt Interactive GCC compiler

    笔记
  • 这太简单了(请参阅免责声明)。 vtable实际上是一种对象描述符,而不仅仅是函数指针的 vector ,并且在虚拟继承的情况下,对象中可能有多个vptr。出于此答案的目的,所有这些都不相关。
  • 只读数据节通常称为.rodata,但仍通常将其描述为“文本”节。这是我警告过份的简化之一。
  • 对于动态加载的库,动态加载程序将模块加载到内存中后,初始化代码将由动态加载程序执行,然后返回程序执行。这通常在main()启动之后很长一段时间。但是同样,这与这里无关。
  • 关于c++ - G++无法内联多态方法吗?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/34357636/

    29 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com