【问题标题】:Non-virtual interface? (Need a very performant low level abstraction)非虚拟界面? (需要一个非常高性能的低级抽象)
【发布时间】:2012-07-03 08:42:35
【问题描述】:

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

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

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

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

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

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

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

【问题讨论】:

  • @JamesMcNellis 感谢您的提示。我怎么没想到?你的意思是:template<int V> class Parser{...},然后是Parser<1>,而不是ParserV1
  • (JamesMcNellis 写了一条评论,他删除了。他建议使用模板类。)
  • 澄清一下 - 您是否尝试针对可能随时调用任何公共成员函数但其中少数比其他函数使用得更多的情况进行优化?
  • 你做对了。这是一个折衷方案:您放弃了运行时动态绑定以实现编译时动态绑定。也许如果速度是绝对必要的,你可以“手动内联”小函数的所有代码......这意味着维护意大利面条函数,但解析器都已经这样了。 :)

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


【解决方案1】:

首先,当然,您应该分析您的代码,以确定在您的特定情况下 vcall 对性能的影响有多大(除了可能较弱的优化之外)。

抛开优化问题不谈,我几乎可以肯定,用调用 compile 的 switch 替换虚函数调用(或通过指针变量调用函数,几乎相同)不会获得任何显着的性能提升- 不同情况下的时间已知函数。

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

  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

【讨论】:

  • 首先,感谢您的回答。在第 2 点中,您的意思是整数模板参数吗?还是一个具体的实现类,比如他的回答中建议的用户“templatetypedef”?
  • @leemes:模板参数是class。 IE。 - 应该使用的解析器的具体类来解析数据。 @templatetypedef 的提议有点过于复杂,恕我直言,因为它仍然使用 virtual 函数,这扼杀了整个想法。
  • 好的。但是我仍然在您的第二个解决方案中看到以下问题:一开始,当我识别版本时(当然是在运行时),我实例化了具体的解析器实现。到目前为止,一切都很好。但是客户端中的成员,在哪里存储这个实例,也必须是一个具体的类型,不是吗?所以我需要一个有点无类型的指针并切换版本每次我使用解析器实例化。我误解了你的想法吗?
  • 好吧,我误解了你的想法。您正在谈论使 client class 成为模板类,模板参数取决于版本。好主意,是的。因此,最终,每当客户端在从客户端的客户端到客户端的一次调用中对解析器执行大量调用时,这都会最大限度地减少虚拟(和非内联)函数的数量。 (事实上​​,我在解析器之上的架构中有更多层。)
  • 好的,我明白了。这非常很好。谢谢!
【解决方案2】:

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

这样做的问题是您使用了额外的内存(包装器对象占用了空间),而且您在调用解析器函数时可能还会涉及至少一个额外的间接调用,因为调用会进行

客户端→包装器→实现

根据您从客户端调用方法的频率,此实现可能会很好地工作。

使用模板,可以非常简洁地实现包装层。思路如下。假设您有方法 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 模板即可为您生成所有包装代码。

希望这会有所帮助!

【讨论】:

  • 我没有看到直接在解析器类中使用虚拟方法的优势,因为这两种解决方案都需要从客户端到最终实现的虚拟调用。仅在另一个时间点/代码。是否可以将您的想法与我的方法合并 4. 经常使用的方法使用我的解决方案,而很少使用的方法使用您的解决方案?
  • @leemes- 优点是解析器类中对其自身的所有调用都可以内联,这意味着可以最大限度地内联实现。您仍然有来自客户端的虚拟调用,但最终实现要快得多。我误解了你的问题吗?
  • 我试图完全避免虚拟通话。我看到了一种可能的选择,即切换我实现的不同版本(我知道,糟糕的代码风格......),并将实现实例转换为特定的类;然后调用可以(可以吗?)内联的方法。像这样:if (version == 2) ((ParserV2*)parser)-&gt;func(); else if ...
  • @leemes: if 块将比virtual 函数慢得多。如果你需要持有一个指向未知实现的指针,你不能避免更多的virtual 函数超过 templatetypedef 所说的。
  • @leemes:到目前为止,避免不可内联性的最好方法是不使用虚函数,但在大多数情况下这样做是荒谬的,并且会使程序运行缓慢。你不希望你的程序变慢吗?改用虚函数。
猜你喜欢
  • 1970-01-01
  • 2014-12-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-10-22
  • 1970-01-01
  • 2011-09-01
相关资源
最近更新 更多