非虚拟接口?(需要一个非常高性能的低级抽象)



我正在尝试在应用程序架构中非常低的级别对我的代码进行微优化。所以这是我的具体方案:

  • 我有一个解析器类,用于解析图形文件(节点、边、邻接条目等)
  • 文件格式是版本化的,因此每个版本都有解析器,这些解析器作为单独的类实现(ParserV1,ParserV2,...)。
  • 解析器为应用程序中的某个上层提供相同的功能。因此,它们实现相同的"接口"。
  • 在C++中,我将这样的接口实现为抽象类,所有函数都是纯虚拟的。
  • 由于虚拟函数需要另一个内存查找,并且不能在编译时静态绑定,而且 - 更重要的是 - 不允许在解析器类中内联小方法,因此使用经典的子类化习惯用法不会带来我所能达到的最佳性能。

[在描述我可能的解决方案之前,我想解释一下我为什么要在这里进行微优化(你可以跳过这一段):解析器类有很多小方法,其中"小"意味着它们没有做太多事情。它们中的大多数只从缓存的位流中读取一个或两个字节,甚至只读取一个位。因此,应该能够以非常非常有效的方式实现它们,其中函数调用在内联时只需要少量机器命令。 这些方法在应用程序中经常被调用,因为它们在一个非常大的图形(全球道路网络)中查找节点属性,每个用户请求可能会发生大约一百万次,并且这样的请求应该尽可能快。

这是去哪里的路?我可以看到以下解决问题的方法:

  1. 用纯虚拟方法编写一个接口并对其进行子类化。性能将受到影响。
  2. 不要编写这样的接口。每个解析器自行定义相同的方法。在上层(使用解析器)具有指向每个版本子类的指针(作为成员)。一开始,实例化应该使用的特定解析器。使用开关块,并在访问函数时将解析器实例强制转换为显式子类。性能会更好吗?(IF/交换机块与虚拟表查找)。
  3. 混合使用两种解决方案1. + 2.:为很少使用的方法编写一个使用纯虚拟方法的接口,其中性能不是很关键。如果它很关键,请不要提供虚拟方法,而是使用第二种方法。
  4. 改进2.:在抽象类中提供非虚拟方法;在抽象类中保留版本号作为成员变量(一种自己的运行时类型信息),并在这些方法中实现if/switch块和强制转换;然后在子类中调用方法。这提供了内联绑定和静态绑定。

有没有更好的方法来解决这个问题?这有什么成语吗?

澄清一下,我有很多与版本无关的函数(至少到目前为止),因此非常适合某个超类。我将对大多数函数使用标准的子类化设计,而这个问题只涉及要优化的版本相关函数的解决方案。(其中一些不是经常调用,在这些情况下我当然可以使用虚拟方法。除此之外,我不喜欢让解析器类决定哪些方法需要高性能,哪些方法不需要。(尽管可以这样做。

一个可能效果很好的选项如下:让每个解析器类定义具有相同签名的方法,但完全独立于其他类。 然后,引入一个辅助类层次结构,该层次结构以虚拟方式实现所有这些相同的功能,然后将每个方法调用转发到具体的解析器对象。 这样,解析器的实现就获得了内联的所有好处,因为从类的角度来看,所有调用都可以静态解析,而客户端可以获得多态性的好处,因为任何方法调用都将动态解析为正确的类型。

这样做的问题是你使用了额外的内存(包装器对象占用空间),当你调用解析器函数时,你也可能至少涉及一个额外的间接寻址,因为调用会去

客户端→包装器→实现

根据从客户端调用方法的不频繁程度,此实现可能非常有效。

使用模板,可以非常简洁地实现包装层。 这个想法如下。 假设您有方法 fA、fB 和 fC。 首先定义一个基类,如下所示:

class WrapperBase {
public:
    virtual ~WrapperBase() = 0;
    virtual void fA() = 0;
    virtual void fB() = 0;
    virtual void fC() = 0;
};

现在,将以下模板类型定义为子类:

template <typename Implementation>
    class WrapperDerived: public WrapperBase {
private:
    Implementation impl;
public:
    virtual void fA() {
        impl.fA();
    }
    virtual void fB() {
        impl.fB();
    }
    virtual void fC() {
        impl.fC();
    }
};

现在,您可以执行以下操作:

WrapperBase* wrapper = new WrapperDerived<MyFirstImplementation>();
wrapper->fA();
delete wrapper;
wrapper = new WrapperDerived<MySecondImplementation>();
wrapper->fB();
delete wrapper;

换句话说,编译器只需实例化WrapperDerived模板即可为您生成所有包装器代码。

希望这有帮助!

首先,当然,您应该分析您的代码,以找出在特定情况下vcalls的性能损失程度(除了潜在的较弱的优化)。

撇开优化主题不谈,我几乎可以肯定,通过将虚拟函数调用(或通过指针变量调用函数,几乎相同)替换为在不同情况下调用编译时已知函数的开关,您不会获得任何显着的性能提升。

如果您真的想要显着改进 - 恕我直言,这些是最有希望的变体:

  1. 尝试重新设计您的界面以启用更复杂的功能。例如,如果您有一个读取单个顶点的函数 - 将其修改为一次读取(最多)N 个顶点。等等。

  2. 您可以将整个解析代码(使用解析器)设置为template类/函数,该类/函数将使用模板参数来实例化所需的解析器。在这里,您既不需要界面,也不需要虚拟功能。在最开始(您标识版本的地方) - 放置一个switch,对于每个识别的版本,请使用适当的模板参数调用此函数。

从性能的角度来看,后者可能会更好,OTOH这会增加代码大小

编辑:

下面是 (2) 的示例:

template <class Parser>
void MyApplication::HandleSomeRequest(Parser& p)
{
    int n = p.GetVertexCount();
    for (iVertex = 0; iVertex < n; iVertex++)
    {
        // ...    
        p.GetVertexEdges(iVertex, /* ... */);    
        // ...    
    }
}
void MyApplication::HandleSomeRequest(/* .. */)
{
    int iVersion = /* ... */;
    switch (iVersion)
    {
    case 1:
        {
            ParserV1 p(/* ... */);
            HandleSomeRequest(p);
        }
        break;
    case 2:
        {
            ParserV2 p(/* ... */);
            HandleSomeRequest(p);
        }
        break;
    // ...
    }
}

ParserV1ParserV2等没有virtual函数。它们也不继承任何接口。他们只是实现一些功能,例如 GetVertexCount .

相关内容

最新更新