【问题标题】:C++ devirtualization at runtime?运行时的 C++ 去虚拟化?
【发布时间】:2015-05-18 23:58:42
【问题描述】:

是否有技术/库允许灵活地拥有类层次结构(具有 virtual 函数),一旦对象类型在运行时确定,就允许函数调用的去虚拟化?

举个简单的例子,假设我有一个程序从某个配置文件中读取形状类型(圆形、矩形、三角形等),以构建所述形状的某种数据结构(例如,vector<shape*>):

class shape {
public:
    virtual void draw() const = 0;
    // ...
};

class circle : public shape {
public:
    void draw() const;
    // ...
};

// ...
vector<shape*> shapes;

显然,如果我想画出所有的形状,我可以这样做:

for ( auto&& s : shapes )
    s->draw();

每次对shapes 进行这样的迭代时,都会调用virtual 函数来为每个形状调用draw()

但是假设一旦创建了shapes,它永远在程序的整个生命周期内都不会再次改变;并进一步假设draw() 将被调用很多 次。如果知道 实际 形状后,如果有一种方法可以“去虚拟化”对draw() 的调用在运行时,那就太好了。

我知道在编译时去虚拟化virtual 函数调用的优化技术,但我没有问这个问题。

如果有一个巧妙的技巧可以直接在 C++ 中执行此操作,我会非常感到惊讶,因为这样做的一种方法是在运行时修改内存中的机器代码。但是是否有一些 C++ 库可以实现这样的事情?

我想像这样的事情可能可以通过 LLVM 实现,因为它允许在运行时生成机器代码。有人为此使用过 LLVM 吗?也许是在 LLVM 之上分层的框架?

注意:解决方案必须是跨平台,即至少使用 gcc/clang VC++。

【问题讨论】:

  • 如果列表中有不同的形状,这种去虚拟化将如何工作?它会(从概念上)将 for 循环内的 s->draw() 调用替换为 (if type == Circle) s->circle::draw(); else if (type == Square) s->square::draw();指令类型?如果是这样,我不确定这一定会更有效。
  • 如果没有进一步的 shape 子类,您可以自己“去虚拟化”这个,通过 typedef-ing 一个到另一个。但是,只要有两个或多个子类,除非编译器能够以某种方式向自己证明特定的代码块只会使用特定的子类,否则它必须使用虚函数调用。这不太可能,但可能。在“auto x=new circle;”之后编译器知道,只要手头有“x”,“x->draw()”就可以安全地去虚拟化,而且许多编译器可能已经这样做了。不幸的是,此类优化的范围往往很小。
  • 您是否执行了表明 vtable 跳转具有可衡量的性能影响的分析? vtable 调用仍然非常便宜。根据 Donald Knuth 的说法,过早的优化是万恶之源。
  • 我从来没有听说过一种自动化的方式来做到这一点,但您总是可以通过根据对象类型将对象放入不同的列表来手动完成。然后,您可以遍历每个类型的列表,为每个列表调用适当的非虚拟方法。
  • 虚拟通话本身基本上是免费的。几次调用后,分支预测器将其拾取,并推测性地执行正确的功能,而无需等待 vtable 查找 - IOW,您的去虚拟化器已经存在,在硬件中实现。真正的代价是错过了内联的潜力,但这需要在运行时对函数进行完全重新编译和优化,所以它绝对是 JIT 材料(不仅仅是修补函数体中的一些位)。

标签: c++ optimization llvm virtual-functions llvm-c++-api


【解决方案1】:

我相当肯定没有魔法这种东西,“在这里编译器,没有更多我要定义的子类,这个列表也不会改变,所以消除虚函数调用开销” 之类的东西。

在性能极其关键的情况下,您可以做的一件事是对形状列表进行排序,以帮助处理虚拟调用。例如,不是像圆形、矩形、三角形、矩形、三角形、正方形等子类型的零星模式,而是要重新排列这些类型以形成如下形式:圆形、圆形、圆形、圆形、...、方形、 square, square, ...等。这有效地优化了分支预测。我不知道这种方法是否仍然适用,或者在最新的架构和优化器中产生很大的影响,但至少在不久前我还活着的时候,它非常有用。

关于 JIT,我一直在探索该领域。我不一定建议尝试找到一种 JIT 解决方案来神奇地使您的 C++ 代码更快。

相反,我一直在探索它,因为我的软件已经有了一种特定领域的语言,一种可视化的节点 GUI 编程语言,您可以在其中绘制节点(函数)之间的连接,而不是编写代码来制作新的东西,比如着色器和图像过滤器(类似于虚幻引擎 4 的蓝图)。目前它的速度远不及手写本机代码,这就是我对探索代码生成/JIT 路线感兴趣的原因。它目前更像是一个解释器。

我已经为这些尝试了 Tiny C 和 LCC,但我发现它们相当令人失望的一件事是它们的优化器不如您的商业生产编译器那么复杂。我经常得到的结果平均比 MSVC 或 GCC 慢 3 到 4 倍。它们非常棒,因为它们非常轻巧且易于嵌入。

LLVM 似乎是一场绝妙的比赛,只是它非常庞大。在我们的核心级别区域中,我们有这种老派美学,其中旨在最大限度地重用的代码应该尽可能少地重用(以避免对外部包的零星依赖)。我很难将其降低到足以通过这些标准的轻量级,但我仍在研究它。

【讨论】:

  • "这里是编译器,..." 已经错了。我的问题明确包括“在运行时”。编译器显然是编译时的。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-12-26
相关资源
最近更新 更多