在运行时添加字段:在析构函数

时间:2017-12-10 09:13:49

标签: c++ caching c++14 destructor vtable

我想在运行时向(假)类添加非静态字段。

对于例如解耦是有用的systemA.cpp可以向某个int添加Data字段,而systemB.cpp可以将BC添加到同一个Data类。

因为这是一个很长的问题,我将分别讨论我的设计,MCVE,它的缺陷,我的方法和问题。

设计

在我的设计中,内存布局与普通类相同 这是图表: -

enter image description here

在我的用例中,上面的布局比int-int-int ... B-B-B ...快 (我描述了。)

为简单起见,我每个字段锁定64个字节 在使用之前,用户必须将每个所需的字段及其类型注册到我的库中: -

    Ref<int> refInt=addField<int>();  //e.g. systemA.cpp
    Ref<B> refB=addField<B>();        //e.g. systemB.cpp
    Ref<C> refC=addField<C>();

我的库将计算用户每1 Data个实例需要的字段数(在这种情况下为3)。

然后,他们可以分配一些Data个实例,并访问Data的动态字段。

    std::vector<Data> datas=allocate(10);
    refInt.get(datas[0])=8;

要删除Data,用户必须调用任何析构函数(BC&#39 ; s)手动 ^这是目标之一。

代码(MCVE

这是我的代码(库级,简化): -

#include <iostream>
#include <vector>
//---- library -----
void* utilAddAddress(void* current,int offset){
    return reinterpret_cast<char*>(current)+offset;
}
int sizePerInstance=0;//accumulate
struct Data{
    void* mem=nullptr;
    Data(void* pmem){mem=pmem;}
};
template<class T>struct Ref{
    int offset=0;
    T& get(Data data){
         return *static_cast<T*>(utilAddAddress(data.mem,offset));
    }
};
template<class T>Ref<T> addField(){
    Ref<T> reff;
    reff.offset=sizePerInstance;
    sizePerInstance+=64;
    return reff;
}
std::vector<Data> allocate(int numInstance){
    void* oNew= ::operator new(static_cast<size_t>( numInstance*sizePerInstance));  
    std::vector<Data> toReturn;
    for(int n=0;n<numInstance;n++){
        toReturn.push_back(Data(utilAddAddress(oNew,n*sizePerInstance)));
    }
    return toReturn;
}

以下是它的使用方法: -

class B{    };
class C{   
    std::string danger;    
};
int main(){
    Ref<int> refInt=addField<int>(); //offset = 0
    Ref<B> refB=addField<B>();       //offset = 64
    Ref<C> refC=addField<C>();       //offset = 128
    std::vector<Data> datas=allocate(10);
    refInt.get(datas[0])=8;
    //destructor ?????
}

问题

它有效,但我找不到有效销毁大量Data个实例的方法。

要销毁Data个实例,我必须调用每个字段的析构函数:intBC
某些字段(例如C)可能不是POD类型,因此我无法跳过此阶段。

到目前为止,我发现只有2个选择。 (我假设每个实例的每个字段都是构造的): -

  1. 按此顺序调用析构函数:[0]int [0]B [0]C [1]int [1]B [1]C ...
  2. 按此顺序调用析构函数:[0]int [1]int [2]int ... [0]B [1]B ... [0]C [1]C ...
  3. 第一选择

    这是一种可行的方式(草案): -

    std::vector<std::function<......>> deleters;
    template<class T>Ref<T> addField(){
        ......
        deleters.push_back([](......){
            static_cast<T*>(......)->~T();
        });
        ......
    }
    

    要适当地调用所有析构函数,我会: -

    for(int n=0;n<10;n++){
        for(int m=0;m<3;m++){
            deleters[m](data[n]);
        }
    }
    

    我将遭受v-table成本。 (我描述)

    第二选择

    我将遭遇缓存未命中,如下图所示。 (我描述)

    enter image description here

    问题

    当删除大量连续的Data时,如何有效地调用所有适当的析构函数?

    我不期待完整的代码。粗略的描述/片段就足够了。

    旁注:

    • 在实际情况中,它通常会分配~4000个实例。 Data最多包含30个字段。
    • 图书馆位于游戏引擎核心。它支持许多类型,例如Data1 Data2 ...
    • 作为临时措施,我放弃支持非POD字段。

1 个答案:

答案 0 :(得分:1)

最快的方法是通过leveragenig创建一个O(1)自定义分配器。

进入数据的所有内容都必须使用特定的分配器,std::stringtypedef std::basic_string<char>,它同样具有默认分配器。

using CBString = std::basic_string<char, std::char_traits<char>, CBAllocator<char>>;

现在,当Data被销毁时,只需让CBAllocator删除其所有池。

您还可以根据std::is_trivially_destructible将字段拆分为两组。或者创建2个不同的数据,或者使其中一个没有std::is_trivially_destructible相邻,以改善缓存局部性。现在,您可以忽略通过std::is_trivially_destructible的析构函数。