【问题标题】:How can I avoid a virtual call when I know the type?知道类型后如何避免虚拟通话?
【发布时间】:2013-10-30 04:19:33
【问题描述】:

考虑以下代码sn-p:

struct Base { virtual void func() { } };
struct Derived1 : Base { void func() override { print("1"); } };
struct Derived2 : Base { void func() override { print("2"); } };

class Manager {
    std::vector<std::unique_ptr<Base>> items;

    public:
        template<class T> void add() { items.emplace_back(new T); }
        void funcAll() { for(auto& i : items) i->func(); }
};

int main() {
    Manager m;
    m.add<Derived1>();
    m.add<Derived2>();
    m.funcAll(); // prints "1" and "2"
};

我正在使用virtual 调度,以便从多态对象的std::vector 调用正确的override 方法。

但是,我知道多态对象是什么类型,因为我在 Manager::add&lt;T&gt; 中指定了它。

我的想法是通过获取成员函数T::func() 的地址并将其直接存储在某处来避免virtual 调用。但是这是不可能的,因为我需要将其存储为 void* 并将其转换回 Manager::funcAll(),但我当时没有类型信息。

我的问题是:在这种情况下,对于多态性,我似乎拥有比平常更多的信息(用户在 Manager::add&lt;T&gt; 中指定派生类型 T) - 有什么方法可以使用此类型信息来防止看似不需要的virtual 电话? (不过,用户应该能够创建自己的类,这些类派生自其代码中的Base。)

【问题讨论】:

  • "我有什么办法可以使用这种类型的信息来防止看似不需要的虚拟呼叫?"不删除该信息? (unique_ptr&lt;Base&gt; 在此处输入擦除)。无论如何,“获取成员函数T::func() 的地址并直接将其存储在某处”与虚拟调用几乎相同,只是您需要做更多的工作。
  • @R.MartinhoFernandes:我明白。然而,我看不到存储派生自 T 的类型的另一种方式,即使在 Manager::add&lt;T&gt; 中指定了类型之后 - 请记住,用户应该定义自己的派生自 Base 的类型,所以我不不知道这些类型会是什么。
  • 所以你想要的在语言中是不可能实现的(然而,编译器可以在这个示例代码中做这样的优化,但我怀疑你在实践中能找到)。这是一个经典的“你不能吃蛋糕也不能吃”的情况:要么删除类型,要么保留类型。
  • @R.MartinhoFernandes:好的,谢谢您的解释。
  • 只是一条建议,不要使用继承,我想你会发现大多数时候使用模板和/或组合(而不是继承)就足够了,而且更好。举例来说,大多数标准库不使用继承

标签: c++ optimization c++11 polymorphism virtual


【解决方案1】:

在这种情况下有时可以提供帮助的一个技巧是按类型对向量进行排序(您应该能够使用 add() 函数中可用的类型知识来强制执行此操作)如果元素的顺序不否则没关系。如果您主要是为了调用虚函数而迭代向量,这将有助于 CPU 的分支预测器预测调用的目标。或者,您可以在管理器中为每种类型维护单独的向量并依次迭代它们,这具有类似的效果。

您的编译器的优化器也可以帮助您处理此类代码,尤其是在它支持 Profile Guided Optimization (POGO) 的情况下。编译器可以在某些情况下对调用进行去虚拟化,或者使用 POGO 可以在生成的程序集中做一些事情来帮助 CPU 的分支预测器,比如测试最常见的类型,并对那些回退到间接调用的直接调用不太常见的类型。

这是一个测试程序的结果,它说明了按类型排序的性能优势,Manager 是您的版本,Manager2 维护一个按 typeid 索引的向量哈希表:

Derived1::count = 50043000, Derived2::count = 49957000
class Manager::funcAll took 714ms
Derived1::count = 50043000, Derived2::count = 49957000
class Manager2::funcAll took 274ms
Derived1::count = 50043000, Derived2::count = 49957000
class Manager2::funcAll took 273ms
Derived1::count = 50043000, Derived2::count = 49957000
class Manager::funcAll took 714ms

测试代码:

#include <iostream>
#include <vector>
#include <memory>
#include <random>
#include <unordered_map>
#include <typeindex>
#include <chrono>

using namespace std;
using namespace std::chrono;

static const int instanceCount = 100000;
static const int funcAllIterations = 1000;
static const int numTypes = 2;

struct Base { virtual void func() = 0; };
struct Derived1 : Base { static int count; void func() override { ++count; } };
int Derived1::count = 0;
struct Derived2 : Base { static int count; void func() override { ++count; } };
int Derived2::count = 0;

class Manager {
    vector<unique_ptr<Base>> items;

public:
    template<class T> void add() { items.emplace_back(new T); }
    void funcAll() { for (auto& i : items) i->func(); }
};

class Manager2 {
    unordered_map<type_index, vector<unique_ptr<Base>>> items;

public:
    template<class T> void add() { items[type_index(typeid(T))].push_back(make_unique<T>()); }
    void funcAll() { 
        for (const auto& type : items) {
            for (auto& i : type.second) {
                i->func();
            }
        }
    }
};

template<typename Man>
void Test() {
    mt19937 engine;
    uniform_int_distribution<int> d(0, numTypes - 1);

    Derived1::count = 0;
    Derived2::count = 0;

    Man man;
    for (auto i = 0; i < instanceCount; ++i) {
        switch (d(engine)) {
        case 0: man.add<Derived1>(); break;
        case 1: man.add<Derived2>(); break;
        }
    }

    auto startTime = high_resolution_clock::now();
    for (auto i = 0; i < funcAllIterations; ++i) {
        man.funcAll();
    }
    auto endTime = high_resolution_clock::now();

    cout << "Derived1::count = " << Derived1::count << ", Derived2::count = " << Derived2::count << "\n"
         << typeid(Man).name() << "::funcAll took " << duration_cast<milliseconds>(endTime - startTime).count() << "ms" << endl;
}

int main() {
    Test<Manager>();
    Test<Manager2>();

    Test<Manager2>();
    Test<Manager>();
}

【讨论】:

    【解决方案2】:

    如果我理解得很好,您希望获取对象类的 add 方法根据该对象类将正确的函数存储在您的向量中。 你的向量只包含函数,没有关于对象的更多信息。

    您有点想在调用虚拟调用之前“解决”它。 这在以下情况下可能很有趣:该函数随后被多次调用,因为您不必每次都解决虚拟问题。

    因此,您可能希望使用与“虚拟”类似的过程,即使用“虚拟表”。 virtual 的实现是在底层完成的,与你想出的任何东西相比都非常快,所以同样,在它变得有趣之前,函数应该被调用很多次。

    【讨论】:

      【解决方案3】:

      但是,我知道多态对象是什么类型,因为我在 Manager::add&lt;T&gt; 中指定了它。

      不,你没有。在add 中,您知道要添加的对象的类型;但是您可以添加不同类型的对象,就像您在示例中所做的那样。 funcAll 无法静态确定元素的类型,除非您将 Manager 参数化为仅处理一种类型。

      如果你确实知道类型,那么你可以非虚拟地调用函数:

      i->T::func();
      

      但是,重申一下,您不能在这里静态确定类型。

      【讨论】:

      • 真的会进行非虚拟通话吗?我认为该语法仅适用于从您的基类调用函数。
      • @R.MartinhoFernandes:是的,这是非虚拟呼叫的语法。演示:ideone.com/JljtCy
      猜你喜欢
      • 1970-01-01
      • 2013-07-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-02-10
      相关资源
      最近更新 更多