代码之家  ›  专栏  ›  技术社区  ›  cppBeginner

缓存大量回调,然后批量调用它们,而不需要v表开销

  •  5
  • cppBeginner  · 技术社区  · 7 年前

    C1 C2 , ... 是回调类。
    CBase 通过回调 CBase::f() .
    所有这些都会覆盖 CBase::f() 具有 final

    我必须注册约50个 C1 ,以及约50个 .
    (参见 @@

    主要目标: allF() , C1::f() / C2::f() 必须调用每个注册实例的。

    Full demo ) :-

    #include <iostream>
    #include <vector>
    class CBase{
        public: virtual void f(){std::cout<<"CBase"<<std::endl;}
    };
    class C1 : public CBase{
        public: virtual void f() final{std::cout<<"C1"<<std::endl;}
    };
    class C2 : public CBase{
        public: virtual void f() final{std::cout<<"C2"<<std::endl;}
    };
    

    这是回调注册:-

    //-------- begin registering -----
    std::vector<CBase*> cBase;   
    void regis(CBase* c){
        cBase.push_back(c);
    }
    void allF(){ //must be super fast
        for(auto ele:cBase){
            ele->f();    //#
        }
    }
    int main() {
        C1 a;
        C1 b;
        C2 c;   //@@
        //or ... class C2Extend : public C2{};   C2Extend c;  
        regis(&a);
        regis(&b);
        regis(&c);
        allF();  //print C1 C1 C2
    }
    

    根据剖面结果,如果我可以避免v形台成本 #

    如何做到优雅?

    我糟糕的解决方案

    一种可能的解决方法是:创建多个数组来存储每个数组 CX ( Full demo ):-

    //-------- begin registering -----
    std::vector<C1*> c1s;
    std::vector<C2*> c2s;
    
    void regis(C1* c){
        c1s.push_back(c);
    }
    void regis(C2* c){
        c2s.push_back(c);
    }
    void allF(){ //must be super fast
        for(auto ele:c1s){
            ele->f();    //#
        }
        for(auto ele:c2s){
            ele->f();    //#
        }
    }
    int main() {
        C1 a;
        C1 b;
        C2 c;
        regis(&a);
        regis(&b);
        regis(&c);
        allF();  //print C1 C1 C2
    }
    

    速度非常快。
    经过几个开发周期, C3 , C4
    std::vector<C3*> , std::vector<C4*> , ... 手动
    我的方法导致了可维护性的地狱。

    更多信息(已编辑)

    C1 C20 )

    在实际情况中, C1 , C2
    所有这些都需要特殊初始化( f() )在准确的时间。

    .cpp .
    因此,阵列存储 std::vector<CBase*> cBase;

    例如 map 1:1 , map 1:N map N:N .
    通过使用自定义分配器,我可以实现超凡脱俗的数据局部性。

    更多注意: 我不在乎回调的顺序。(谢谢 火焰枪

    3 回复  |  直到 7 年前
        1
  •  4
  •   Quentin    7 年前

    当您使用模板将其自动化时,您的“糟糕的解决方案”看起来会更好。我们的目标:商店 c1s c2s ,等等。

    为此,我们需要将派生类型映射到连续整数。一种简单的方法是使用一个全局计数器和一个函数模板,该模板在每次实例化时递增并存储它。

    static std::size_t typeIndexCounter = 0;
    
    template <class>
    std::size_t indexForType() {
        static std::size_t const index = typeIndexCounter++;
        return index;
    }
    

    indexForType<T>() 将为保留新索引 T ,并在后续调用中返回相同的值。

    f 在他们身上。

    struct Group {
        using CbVec = std::vector<void *>;
    
        void (*call)(CbVec &);
        CbVec callbacks;
    };
    
    static std::vector<Group> groups;
    

    call 将持有一个函数,该函数在指针上迭代,并对其进行降级和调用 . 就像您的解决方案一样,这会将对单个类型的所有调用分解为一个虚拟调用。

    CbVec 可以容纳 CBase * void * ,但我稍后会解释这个选择。

    groups 请求时 Group 对于某些类型:

    template <class T>
    Group &groupFor() {
    
        std::size_t const index = indexForType<T>();
        if(index < groups.size())
            // Group already exists, return it
            return groups[index];
    
        assert(
            index == groups.size() &&
            "Something went wrong... Did someone call detail_callbacks::indexForType?"
        );
    
        // Register the new group, with its downcasting function
        groups.push_back({
            [](Group::CbVec &callbacks) {
                for(void *p : callbacks)
                    static_cast<T*>(p)->f();
            },
            {}
        });
    
        // Return the new group
        return groups.back();
    }
    

    而不是 CBase数据库* 问题是,其中对性能敏感的向下转换变成了无操作,而派生转换的基可能需要指针调整(以及在虚拟继承的情况下的进一步复杂化)。

    namespace detail_callbacks ,我们只需要把这些部分放在一起:

    template <
        class T,
        class = std::enable_if_t<std::is_base_of<CBase, T>::value>
    >
    void regis(T *callback) {
        detail_callbacks::groupFor<T>().callbacks.push_back(static_cast<void*>(callback));
    }
    
    void allF() {
        for(auto &group : detail_callbacks::groups)
            group.call(group.callbacks);
    }
    

    你来了!新的派生回调现在自动注册。

    See it live on Coliru

        2
  •  3
  •   Caleth    7 年前

    您可以将全局变量拉入在派生类型上模板化的类中,在实例化时,确保它是整个调用的一部分。

    typedef void(*Action)(); // function pointer type, receives static call 
    std::set<Action> allFs; 
    
    template<typename T>
    struct FRegistry 
    {
        static std::vector<T*> ts;
        static void doF() 
        { 
            // loop over the derived type, so no need for virtual
            for (T * t : ts) { t->f(); } 
        }
        static void regis(T * item) 
        { 
            allFs.insert(&FRegistry::doF); // Ensure the global call includes this instantiation
            ts.push_back(t); // register the instance
        }
    }
    
    template<typename T>
    std::vector<T*> FRegistry<T>::ts = {}; // member initialisation
    
    template <typename T>
    regis(T * t)
    { 
        FRegistry<T>::regis(t); 
    }
    
    void allF()
    { 
        for (Action a : allFs) { a(); } // call each doF
    }
    

    int main() {
        C1 a;
        C1 b;
        C2 c;
        regis(&a);
        regis(&b);
        regis(&c);
        allF();  //print C1 C1 C2
    }
    
        3
  •  1
  •   Fire Lancer    7 年前

    虚拟调用已经是一个非常简单和快速的实现,因此如果这是一个问题,没有结构变化,任何事情都不够快。值得注意的是,我不会期望一个简单的 std::function 或者手动使用函数指针是一个很大的收获。实际上,虚拟呼叫可能看起来像:

    class CBase{
        // Compiler generated
        struct Vtable
        {
            void (CBase::*f)();
        };
        public: virtual void f(){std::cout<<"CBase"<<std::endl;}
    
        // Compiler addded instance field
        Vtable *vtable;
    };
    class C1 : public CBase{
        public: virtual void f() final{std::cout<<"C1"<<std::endl;}
        // Compiler generated static data to initialise vtable member
        static Vtable C1::type_vtable = { &C1::f };
    };
    
    
    CBase *ptr = vector.front();
    ptr->f();
    // Gets compiled as
    ptr->(*ptr->vtable->f)();
    

    因此,在代码级别,需要进行一些额外的内存读取,然后通过函数指针调用函数。然而,这阻碍了许多优化。在编译器级别,它不能再内联函数。在您需要的CPU级别 ptr->vtable 在CPU缓存中并冒着分支预测失败的风险,与直接函数调用相比,这两种方法的成本都远远高于少量内存读取可能意味着的成本。如果您有许多基类,并且它们在容器中是随机排序的(CPU可能会不断猜测下一个基类是什么),则情况尤其如此。

    没有设计更改的最优化解决方案更像您展示的那样。完全去掉虚函数/间接函数,并将其存储在单独的容器中。这使得编译器可以在认为值得的情况下内联函数调用,并使CPU变得容易。您可能会使用重载或模板,所以在最坏的情况下只有一个地方可以调用(使用模板,取决于需要更聪明的东西)。

    class Register
    {
        std::vector<C1*> c1;
        std::vector<C2*> c2;
    
    
        void regis(C1 *c1);
        void regis(C2 *c2);
        //etc.
    };
    

    请注意,您更改了调用对象的顺序。您按类类型对它们进行了排序,但之前的顺序与 regis 已调用。

    仅按类类型排序(可以使用 typeid

    “配置文件引导优化”(PGO,在编译器中查找,例如MSVC和GCC可以做到这一点)也可能有帮助,需要一些额外的构建时间。它允许编译器根据实际运行的代码进行优化。我没有详细研究过为实际项目生成的代码,但我知道MSVC至少可以使用类似switch语句的命令来“内联”常见的虚拟调用 类型ID ,允许更好的优化,并可能更好地使用现代CPU。