虚函数调用的优化

3

我有一个关于虚函数调用优化的问题。我在某处读到过(但现在找不到这篇文章了)可能可以通过使用类似于以下结构来优化掉 v-table 查找:

// Base.h
class Base
{
public:
    virtual void Foo() = 0;
};

// Concrete.h
class Concrete : public Base
{
public:
    virtual void Foo()
    {
        // do something;
    }
};

//Some.h
extern Base* const g_object;

// Some.cpp
Concrete on_stack_concrete;

Base* const g_object = &on_stack_concrete;

这个技巧是使用指向堆栈上分配的变量(非动态分配)的const指针。编译器必然会优化掉这个操作。因此,每当用户调用g_object->Foo()时,“// do something”部分将被执行,无需进行虚函数查找。
这是真的还是假的?
提前感谢任何回复。
编辑:
这种构造的可能用途是限制具体实现的接口。当然,有人会认为“受限”方法应该是私有的,但有时库的其他模块需要访问对象的这些公共附加方法,而不允许用户操纵它们。因此,例如使用#define,可以创建类似于以下代码的代码:
// Some.cpp
#ifdef _WIN32
Win32Concrete concrete;
#elif defined _UNIX
UnixConcrete concrete;
#endif

Base* const g_global = &concrete;

事实上,这些类的声明只能在CPP文件中定义,因此用户不知道它们的存在。

问题不在于为什么要首先使用这样的常量指针,而是是否有可能优化掉这种情况中的虚函数表查找。


2
这可能与编译器有关,您应该能够通过查看编译器生成的汇编代码来验证这一点。 - marcinj
2
为什么即使指针总是指向同一个对象,我们还要使用指针? - Bo Persson
@BoPersson,我现在可以想到一个很好的应用,例如跨平台库,只向用户公开抽象接口,后端根据某些#defines使用多个实现来支持各种平台。当然,这需要为每个平台进行多次编译,使用不同的定义集,但对于最终用户来说并没有任何区别。 - Adrian Lis
1
如果Win32Concrete类对Some.h的客户端不可见,那么他们很可能无法优化掉vtable,因为他们看不到它的“真实情况”,因为直到链接时才会决定。无论如何,vtable优化并不是保证的,因此除了首先没有vtable之外,没有其他强制它的方法。 - Raymond Chen
3个回答

4

您似乎误用了virtual

virtual实现运行时多态性,而您描述的情况并不使用或需要它。在任何编译环境中都不太可能同时存在Win32ConcreteUnixConcrete

建议改为:

// Some.cpp
#ifdef _WIN32
Win32Concrete concrete;
#elif defined _UNIX
UnixConcrete concrete;
#endif

Base* const g_global = &concrete;

使用:

// CommonHeader.h
#ifdef _WIN32
typedef Win32Concrete Concrete;
#elif defined _UNIX
typedef UnixConcrete Concrete;
#endif

现在您的函数不需要是虚函数。

全局变量以这种方式初始化的目的是避免在需要时将其作为参数传递给函数,因此这种情况不会发生。它是全局的,因此可以在任何地方访问,因此在我的理解中,“上下文”不会改变。 - Adrian Lis
@AdrianLis 如果您直接访问全局变量,那么为什么还要创建 Base* const g_object 呢?您可以使用 on_stack_concrete.foo()。在这种情况下,您可以避免查找。 - Drew Dormann
因为具体类可以向用户公开不同(附加)的接口,而他不应该直接使用这些接口。这就是为什么基类指针存在的原因,它将接口限制在必要的内容上。我将编辑问题以显示可能的用法。 - Adrian Lis
我知道这样的解决方案,但是您先生又错过了外部暴露的一个重要点。基础是库的用户的公共接口。具体实现是库的内部实现的细节,每个实现可能会有很大的不同。当然,在内部需要“更丰富”的接口时,我会使用具体变量,但是无论内部实现使用什么,用户都应该获得统一的接口,而且由于这在运行时不会改变并且以这种方式编译,所以问题是v-table查找是否不可避免。 - Adrian Lis
当然,我可以使用适配器或包装器来限制接口的具体实现,但这将需要为每个新实现执行大量的预处理器宏和额外的编码。在这种情况下,使用运行时多态性更容易,但是虚拟函数的不必要成本会出现。这就是为什么我问所读到的解决方案是否可行的原因。 - Adrian Lis
@AdrianLis 我建议你重新审视一下为什么要使用虚函数。或者提出一个新问题,询问如何在没有虚函数的情况下实现编译时接口。 - Drew Dormann

2
这是Doom 3源代码中使用的方法(例如https://github.com/id-Software/DOOM-3-BFG/),比如neo/framework/FileSystem.h 定义了以下内容:
extern idFileSystem *       fileSystem;

neo/framework/FileSystem.cpp 定义了这个:
idFileSystemLocal   fileSystemLocal;
idFileSystem *      fileSystem = &fileSystemLocal;

我找到的唯一关于它的讨论是这个:http://fabiensanglard.net/doom3/ idTech4高级对象都是带有虚方法的抽象类。一般情况下,这会导致性能损失,因为在运行时每个虚方法地址都必须在vtable中查找后才能调用它。但是有一个“技巧”可以避免这种情况。由于在数据段静态分配的对象具有已知的类型,因此编译器可以在调用commonLocal方法时优化掉vtable查找。

1
最简单的解决方法是将需要访问受限方法的类定义为Concrete类的友元。这样,它们就可以完全访问该类,而其他所有人只能获得公共访问权限。
如果这不可行,您可以将实现放在基类中,并将所有受限方法设置为受保护的,然后派生一个特殊的类,以公开受保护的方法。
class Concrete
{
public:
    void foo() { ... }
protected:
    void bar() { ... }
};

class ConcretePrivate : public Concrete
{
public:
    void bar() { Concrete:: bar(); }
};

ConcretePrivate g_globalPrivate;
Concrete& g_global = g_globalPrivate;

使用g_global的代码只能访问Concrete方法。使用g_globalPrivate的代码可以访问ConcretePrivate方法。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接