You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为何功能等价的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

观察结果

  1. Code1中未生成Base::pr_fn2()、Derived::pr_fn()和Derived::pr_fn3()的汇编代码。
  2. 上述所有函数均被内联到Code1的main()汇编中。
  3. Code2中生成了Interface::pr_fn3()、Base::pr_fn2()、Derived::pr_fn()和Derived::pr_fn3()的汇编代码。
  4. 上述所有函数均被内联到Code2的main()汇编中。
  5. Code2的Derived::pr_fn3()汇编中内联了对Interface::pr_fn3()、Base::pr_fn2()和Derived::pr_fn()的调用。

核心问题

两个程序执行main()时的汇编指令完全一致,为何Code2会生成上述额外函数的汇编代码,而Code1不会?

补充问题

  1. 将Interface::pr_fn3()的实现移至类定义内部后,其汇编不再生成,但Base::pr_fn2()、Derived::pr_fn()、Derived::pr_fn3()的汇编仍存在于Code2中,与Code1的差异为何依然存在?
  2. 将代码编译链接为二进制后,Base::pr_fn2()和Derived::pr_fn()的汇编被移除,但Derived::pr_fn3()的汇编仍保留,原因是什么?
  3. 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,也会保留一份独立实现副本。

补充问题解答

  1. 类内定义Interface::pr_fn3()后仍有差异:
    类内定义的虚函数默认是inline属性,GCC会优先尝试内联,因此Interface::pr_fn3()的独立汇编被消除。但Base::pr_fn2()、Derived::pr_fn()和Derived::pr_fn3()作为虚函数覆盖,GCC依然判定它们可能被外部通过虚表访问,或者因Derived::pr_fn3()显式调用了类内的Interface::pr_fn3(),编译器优化逻辑未完全消除这些函数的独立符号生成。

  2. 编译链接后仅保留Derived::pr_fn3():
    链接阶段的垃圾回收(GC)会移除未被引用的符号。Base::pr_fn2()和Derived::pr_fn()的独立实现未被任何外部代码或虚表引用(main中已完全内联,虚表也被优化掉),因此被GC移除。而Derived::pr_fn3()作为Interface::pr_fn3()的覆盖函数,GCC编译时可能为其保留了符号,或链接器GC规则对显式覆盖的虚函数处理不同,导致其符号被保留。

  3. Clang与GCC表现差异:
    两款编译器的优化器实现逻辑不同。Clang在-O3下会更激进地分析虚函数的调用场景,即便存在类外定义的纯虚函数默认实现,只要能确定函数不会被外部编译单元调用,就会消除所有不必要的独立函数符号,仅保留内联后的main代码。而GCC的虚函数优化策略相对保守,更容易保留可能被外部引用的虚函数符号。

内容的提问来源于stack exchange,提问作者Arkanil Paul

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 11:59:50