为何功能等价的C++虚函数代码在GCC下生成不同汇编?
Code 1
#include <iostream> struct Interface { virtual void pr_fn() = 0; virtual void pr_fn2() = 0; virtual void pr_fn3() = 0; }; struct Base : Interface { void pr_fn2() final { std::cout << "Base\n"; } }; struct Derived : Base { void pr_fn() final { std::cout << "Derived2\n"; } void pr_fn3() final { pr_fn2(); pr_fn(); } }; int main() { Derived d; d.pr_fn3(); return 0; }
Code 2
#include <iostream> struct Interface { virtual void pr_fn() = 0; virtual void pr_fn2() = 0; virtual void pr_fn3() = 0; }; void Interface::pr_fn3() { pr_fn2(); pr_fn(); } struct Base : Interface { void pr_fn2() final { std::cout << "Base\n"; } }; struct Derived : Base { void pr_fn() final { std::cout << "Derived\n"; } void pr_fn3() final { Interface::pr_fn3(); } }; int main() { Derived d; d.pr_fn3(); return 0; }
Code 1 汇编(编译器:x86-64 gcc 14.2,编译选项:-O3)
.LC0: .string "Base\n" .LC1: .string "Derived2\n" main: sub rsp, 8 mov edx, 5 mov esi, OFFSET FLAT:.LC0 mov edi, OFFSET FLAT:std::cout call std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long) mov edx, 9 mov esi, OFFSET FLAT:.LC1 mov edi, OFFSET FLAT:std::cout call std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long) xor eax, eax add rsp, 8 ret
Code 2 汇编(编译器:x86-64 gcc 14.2,编译选项:-O3)
.LC0: .string "Base\n" Base::pr_fn2(): mov edx, 5 mov esi, OFFSET FLAT:.LC0 mov edi, OFFSET FLAT:std::cout jmp std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long) .LC1: .string "Derived\n" Derived::pr_fn(): mov edx, 8 mov esi, OFFSET FLAT:.LC1 mov edi, OFFSET FLAT:std::cout jmp std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long) Derived::pr_fn3(): sub rsp, 8 mov edx, 5 mov esi, OFFSET FLAT:.LC0 mov edi, OFFSET FLAT:std::cout call std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long) mov edx, 8 mov esi, OFFSET FLAT:.LC1 mov edi, OFFSET FLAT:std::cout add rsp, 8 jmp std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long) Interface::pr_fn3(): push rbx mov rax, QWORD PTR [rdi] mov rbx, rdi mov rax, QWORD PTR [rax+8] cmp rax, OFFSET FLAT:Base::pr_fn2() jne .L7 mov edx, 5 mov esi, OFFSET FLAT:.LC0 mov edi, OFFSET FLAT:std::cout call std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long) mov rax, QWORD PTR [rbx] mov rax, QWORD PTR [rax] cmp rax, OFFSET FLAT:Derived::pr_fn() jne .L9 .L11: mov edx, 8 mov esi, OFFSET FLAT:.LC1 mov edi, OFFSET FLAT:std::cout pop rbx jmp std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long) .L7: call rax mov rax, QWORD PTR [rbx] mov rax, QWORD PTR [rax] cmp rax, OFFSET FLAT:Derived::pr_fn() je .L11 .L9: mov rdi, rbx pop rbx jmp rax main: sub rsp, 8 mov edx, 5 mov esi, OFFSET FLAT:.LC0 mov edi, OFFSET FLAT:std::cout call std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long) mov edx, 8 mov esi, OFFSET FLAT:.LC1 mov edi, OFFSET FLAT:std::cout call std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long) xor eax, eax add rsp, 8 ret
观察结果
- Code1中未生成
Base::pr_fn2()、Derived::pr_fn()和Derived::pr_fn3()的汇编代码。 - 上述所有函数均被内联到Code1的
main()汇编中。 - Code2中生成了
Interface::pr_fn3()、Base::pr_fn2()、Derived::pr_fn()和Derived::pr_fn3()的汇编代码。 - 上述所有函数均被内联到Code2的
main()汇编中。 - Code2的
Derived::pr_fn3()汇编中内联了对Interface::pr_fn3()、Base::pr_fn2()和Derived::pr_fn()的调用。
核心问题
两个程序执行main()时的汇编指令完全一致,为何Code2会生成上述额外函数的汇编代码,而Code1不会?
补充问题
- 将
Interface::pr_fn3()的实现移至类定义内部后,其汇编不再生成,但Base::pr_fn2()、Derived::pr_fn()、Derived::pr_fn3()的汇编仍存在于Code2中,与Code1的差异为何依然存在? - 将代码编译链接为二进制后,
Base::pr_fn2()和Derived::pr_fn()的汇编被移除,但Derived::pr_fn3()的汇编仍保留,原因是什么? - Clang 19.1.0对两段代码生成的汇编完全一致,为何GCC的表现存在差异?
问题解答
核心问题解答
Code2生成额外函数汇编的根源是GCC对虚函数定义位置的优化策略差异:
- Code1中所有虚函数的实现都在类内部定义,GCC在-O3优化下能明确这些函数仅被当前编译单元内的代码调用,且完全可以内联到
main中,因此无需生成独立的函数符号。 - Code2中
Interface::pr_fn3()是在类外定义的纯虚函数默认实现,GCC会默认认为这个函数可能被其他编译单元通过基类指针调用(即使当前代码无此场景),因此保留其独立汇编。而Base::pr_fn2()、Derived::pr_fn()和Derived::pr_fn3()作为虚函数的覆盖实现,由于Interface::pr_fn3()的存在,GCC需要确保这些函数有独立符号供虚表引用,即便已内联到main,也会保留一份独立实现副本。
补充问题解答
类内定义
Interface::pr_fn3()后仍有差异:
类内定义的虚函数默认是inline属性,GCC会优先尝试内联,因此Interface::pr_fn3()的独立汇编被消除。但Base::pr_fn2()、Derived::pr_fn()和Derived::pr_fn3()作为虚函数覆盖,GCC依然判定它们可能被外部通过虚表访问,或者因Derived::pr_fn3()显式调用了类内的Interface::pr_fn3(),编译器优化逻辑未完全消除这些函数的独立符号生成。编译链接后仅保留
Derived::pr_fn3():
链接阶段的垃圾回收(GC)会移除未被引用的符号。Base::pr_fn2()和Derived::pr_fn()的独立实现未被任何外部代码或虚表引用(main中已完全内联,虚表也被优化掉),因此被GC移除。而Derived::pr_fn3()作为Interface::pr_fn3()的覆盖函数,GCC编译时可能为其保留了符号,或链接器GC规则对显式覆盖的虚函数处理不同,导致其符号被保留。Clang与GCC表现差异:
两款编译器的优化器实现逻辑不同。Clang在-O3下会更激进地分析虚函数的调用场景,即便存在类外定义的纯虚函数默认实现,只要能确定函数不会被外部编译单元调用,就会消除所有不必要的独立函数符号,仅保留内联后的main代码。而GCC的虚函数优化策略相对保守,更容易保留可能被外部引用的虚函数符号。
内容的提问来源于stack exchange,提问作者Arkanil Paul

