摘要

最近有人问我关于去虚化优化的问题:它们何时发生?我们何时可以依赖去虚化?不同的编译器在去虚化方面有何不同?

通常,这会让我陷入实验的兔子洞。答案似乎是:现代编译器在调用 final 方法时去虚化非常可靠。但有许多有趣的边缘情况——包括一些我还没有想到的——不同的编译器会捕捉到这些边缘情况的不同子集。

当我们知道实例的动态类型

最典型的例子是:

void test() {
    Apple o;
    o.f();
}

即使 Apple::f 是虚函数,这也没有关系;虚函数调用所做的只是在对象的实际动态类型上调用该方法,而在这里我们确切地知道实际动态类型是 Apple。在这种情况下,静态和动态调度应该给我们相同的结果。

足够智能的编译器将使用数据流分析来优化非平凡的案例,例如:

Derived d;
Base *p = &d;
p->f();

事实证明,即使这个简单的技巧也足以愚弄 MSVC 和 ICC。 下一个测试案例是:

Derived da, db;
Base *p = cond ? &da : &db;
p->f();

这超出了 Clang 的能力,但 GCC 实际上可以处理它……直到你在条件语句中进行 Base* 的转换!这里就是 GCC 的分析失败的地方[1]

Derived da, db;
Base *p = cond ? (Base*)&da : (Base*)&db;
p->f();

当我们知道其静态类型的“叶子证明”

假设我们从系统中的其他地方接收一个指针。我们知道它的静态类型(例如 Derived*),但不知道它所指向的对象的实际动态类型。尽管如此,编译器可以去虚化对 Derived::f 的调用,如果它能够证明整个程序中没有任何类型可以覆盖 Derived::f

通过 final 证明

最简单的“叶子证明”是将 Derived 标记为 final

struct Base {
    virtual int f();
};
struct Derived final : public Base {
    int f() override { return 2; }
};
int test(Derived *p) {
    return p->f();
}

类型为 Derived* 的指针必须指向一个“至少是 Derived”的对象实例——即 Derived 或其子类。由于 Derivedfinal,它不允许有子类;因此,实例的实际动态类型必须是 Derived,编译器可以去虚化这个调用。

或者,你可以将特定方法 Derived::f 标记为 final

无论 Derived::f 是在 Derived 中声明的,还是从 Base 继承的,相同的分析都应适用。例如,编译器应该能够同样地去虚化:

struct Base {
    virtual int f() { return 1; }
};
struct Derived final : public Base {};
int test(Derived *p) {
    return p->f();
}

GCC、Clang 和 MSVC 通过了这个测试[2]。ICC 21.1.9 被愚弄了。

通过内部链接证明

名称具有内部链接的类不能在当前翻译单元之外命名。因此,它也不能在当前翻译单元之外派生!只要它在当前 TU 中没有子类——或者至少没有覆盖其方法的子类——对其虚函数的调用就可以去虚化。

例如:

namespace {
    class BaseImpl : public Base {};
}
int test(Base *p) {
    return static_cast<BaseImpl*>(p)->f();
}

如果 p 真正指向一个“至少是 BaseImpl”的对象实例,那么编译器可以证明该实例必须是 BaseImpl。如果 p 没有指向一个“至少是 BaseImpl”的实例,程序将有未定义行为。

这在实际代码库中可能相当常见。通常,头文件中会公开一个基类,然后在一个单独的 .cpp 文件中紧密地作用域化一个或多个派生实现。如果你更进一步,将这些派生实现放入匿名命名空间中,你可能会帮助编译器的去虚化逻辑。当然,根据定义,任何这样的好处将仅限于该单个 .cpp 文件!

另一个类型名称可以具有内部链接的方式是当它是类模板实例化,其中一个模板参数涉及具有内部链接的名称。如果名称 T 具有内部链接,那么 E<T> 也具有内部链接,即使 E 本身具有外部链接——因为你不能命名 E<T> 而不命名 T。(注意,这里的 T 必须是一个“真实名称”;我们不讨论类型别名。)

还有一种可能,类型名称具有外部链接,但编译器可以证明该类型在其他每个 TU 中必须是不完整的。例如:

namespace {
    class Internal {};
}
class External { Internal m; };

其他任何 TU 都允许将 class External; 声明为不完整的类型,但这些 TU 不能完成该类型,因为它们不能命名其数据成员的类型。你不能从不完整的类型派生。因此,所有从 External 派生的类型(如果有)都必须存在于这个 TU 中;如果这里没有,那么这就是一个“叶子证明”!只有 GCC 检测到这种情况。

测试结果表

在后者中,我分别对 Derived::f 直接在 Derived 中定义和 Derived::gBase 继承的情况进行了测试。GCC 经常正确地处理 f,但未能去虚化 g。 我已经为这个问题提交了[3]

测试案例重点GCCClangMSVCICC

one

简单

two

转换为 Base*

three

条件,然后转换

four

转换,然后条件

one

final

f

two

final 方法

three

奇怪的 final 析构函数

four

奇怪的老技巧

five

内部链接类

f

six

内部链接模板参数

f

seven

内部链接基类

f

eight

内部链接成员

f

nine

内部链接带子类

f

ten

本地类

f

其他

Steve Dewhurst 教了我他的“奇怪的老技巧”:虚基类总是以最派生类的上下文构造。因此,如果类 C 有一个虚基类,其所有构造函数都是私有的,那么 C 的任何子类都无法构造自己,因此 C 的任何子类都不存在。(当然,那个虚基类必须将 C 列为其朋友,以便 C 本身可以构造。) 我认为这个技巧是万无一失的,因此构成了一个(非常奇怪的)C 的“叶子证明”;但当然,编译器不会关心追踪这个逻辑上的纠结,即使它是万无一失的。

Last moify: 2022-12-04 15:11:33
Build time:2025-07-18 09:41:42
Powered By asphinx