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

时间:2012-07-02 22:34:48

标签: c++ architecture abstraction idioms micro-optimization

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

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

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

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

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

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

2 个答案:

答案 0 :(得分:3)

可能运行良好的一个选项如下:让每个解析器类定义具有相同签名的方法,但这样做完全独立于其他类。然后,引入一个实现所有这些相同函数的二级类层次结构,然后将每个方法调用转发给一个具体的解析器对象。这样,解析器的实现获得了内联的所有好处,因为从类的角度来看,所有调用都可以静态解析,而客户端可以获得多态性的好处,因为任何方法调用都会动态地解析为正确的类型。

执行此操作的问题是您使用额外的内存(包装器对象占用空间),并且当您调用解析器函数时,您可能还会至少涉及一个额外的间接,因为调用是

  

客户端→包装器→实现

根据您从客户端调用方法的频率,这种实现可能会很好。

使用模板,可以简洁地实现包装层极其。这个想法如下。假设您有方法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模板为您生成所有包装器代码。

希望这有帮助!

答案 1 :(得分:2)

首先,您应该对您的代码进行分析,以确定在您的特定情况下vcalls性能损失的程度(除了可能较弱的优化之外)。

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

如果你真的想要一个显着的改进 - 那些是最有希望的变种恕我直言:

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

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

  3. 后者可能会从性能上看更优越,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