切换到另一个不同的自定义分配器->传播到成员字段

时间:2019-04-01 07:18:00

标签: c++ class memory c++14 allocator

我对程序进行了分析,发现从标准分配器更改为自定义的一帧分配器可以消除我最大的瓶颈。

这是一个虚拟代码段(coliru link):-

class Allocator{ //can be stack/heap/one-frame allocator
    //some complex field and algorithm
    //e.g. virtual void* allocate(int amountByte,int align)=0;
    //e.g. virtual void deallocate(void* v)=0;
};
template<class T> class MyArray{
    //some complex field
    Allocator* allo=nullptr;
    public: MyArray( Allocator* a){
        setAllocator(a);
    }
    public: void setAllocator( Allocator* a){
        allo=a;
    }
    public: void add(const T& t){
        //store "t" in some array
    }
    //... other functions
};

但是,我的一帧分配器有一个缺点-用户必须确保 由一帧分配器分配的每个对象 必须在末尾删除/释放时间步长。

问题

这里是用例的一个例子。

我使用一帧分配器在物理引擎中存储M3(碰撞检测的重叠表面; wiki link)的临时结果。

这是一个片段。
M1M2M3都是流形,但细节程度不同:-

Allocator oneFrameAllocator;
Allocator heapAllocator;
class M1{};   //e.g. a single-point collision site
class M2{     //e.g. analysed many-point collision site
    public: MyArray<M1> m1s{&oneFrameAllocator};
};
class M3{     //e.g. analysed collision surface
    public: MyArray<M2> m2s{&oneFrameAllocator};
};

请注意,我将默认分配器设置为oneFrameAllocator(因为它是CPU节省者)。
因为我仅将M1M2M3的实例创建为临时变量,所以它可以工作。

现在,我想为下一个M3 outout_m3=m3;缓存timeStep的新实例。
(^检查碰撞是刚刚开始还是刚刚结束)

换句话说,我想将已分配一帧 m3复制到位于output_m3已分配堆 #3(如图下面)。

这是游戏循环:-

int main(){
    M3 output_m3; //must use "heapAllocator" 
    for(int timeStep=0;timeStep<100;timeStep++){
        //v start complex computation #2
        M3 m3;
        M2 m2;
        M1 m1;
        m2.m1s.add(m1);
        m3.m2s.add(m2);
        //^ end complex computation
        //output_m3=m3; (change allocator, how?  #3)
        //.... clean up oneFrameAllocator here ....
    }
}

enter image description here

我无法直接分配output_m3=m3,因为output_m3将从m3复制一帧分配器的用法。

我可怜的解决方案是从下至上创建output_m3
下面的代码有效,但是非常乏味。

M3 reconstructM3(M3& src,Allocator* allo){
    //very ugly here #1
    M3 m3New;
    m3New.m2s.setAllocator(allo);
    for(int n=0;n<src.m2s.size();n++){
        M2 m2New;
        m2New.m1s.setAllocator(allo);
        for(int k=0;k<src.m2s[n].m1s.size();k++){
            m2New.m1s.add(src.m2s[n].m1s[k]);
        }
        m3New.m2s.add(m2New);
    }
    return m3New;
}
output_m3=reconstructM3(m3,&heapAllocator);

问题

如何优雅地切换对象的分配器(无需手动传播所有内容)?

赏金描述

  1. 答案不需要基于我的摘要或任何物理方面的内容。我的代码可能无法修复。
  2. IMLI,将分配器类型作为类模板参数(例如MyArray<T,StackAllocator>)传递是不希望的。
  3. 我不介意Allocator::allocate()Allocator::deallocate()的vtable-cost。
  4. 我梦想着有一个C ++模式/工具,它可以自动将分配器传播给类的成员。也许它是operator=(),如 MSalters 所建议,但我找不到合适的方法来实现它。

参考:在收到 JaMiT 的回答后,我发现此问题类似于Using custom allocator for AllocatorAwareContainer data members of a class

3 个答案:

答案 0 :(得分:8)

正当化

这个问题的核心是寻求一种将自定义分配器与多层容器一起使用的方法。还有其他规定,但是考虑到这一点之后,我决定忽略其中的一些规定。他们似乎没有充分的理由妨碍解决方案。这就留下了从标准库std::scoped_allocator_adaptorstd::vector中回答问题的可能性。

也许这种方法的最大变化是抛弃了一个想法,即容器的分配器在构造后需要进行修改(抛弃setAllocator成员)。这个想法总体上似乎是有问题的,在这种情况下是不正确的。查看决定使用哪个分配器的标准:

  • 一帧分配要求在timeStep上循环结束之前销毁对象。
  • 一帧分配无法使用时,应使用堆分配。

也就是说,您可以通过查看相关对象/变量的范围来确定使用哪种分配策略。 (是在循环体内还是在循环体外?)作用域在构造时是已知的,并且不会改变(只要您不滥用std::move)。因此,所需的分配器在构造时是已知的,并且不会更改。但是,当前的构造函数不允许指定分配器。那是要改变的东西。幸运的是,这种更改是引入scoped_allocator_adaptor的自然扩展。

另一个大变化是抛弃MyArray类。存在标准容器可简化您的编程。与编写自己的版本相比,标准容器的实现速度更快(如已完成),并且不易出错(与“这次为我工作”相比,标准容器追求更高的质量标准)。因此,使用MyArray模板,然后使用std::vector

操作方法

本节中的代码段可以加入到一个可编译的源文件中。跳过我之间的评论。 (这就是为什么只有第一个片段包含标头的原因。)

您当前的Allocator类是一个合理的起点。它仅需要一对方法来指示两个实例何时可以互换(即何时两个实例都可以解除分配由它们各自分配的内存)。我还自由地将amountByte更改为无符号类型,因为分配负数的内存没有意义。 (尽管我没有留下align的类型,因为没有迹象表明它将采用什么值。可能应该是unsigned或枚举。)

#include <cstdlib>
#include <functional>
#include <scoped_allocator>
#include <vector>

class Allocator {
public:
    virtual void * allocate(std::size_t amountByte, int align)=0;
    virtual void deallocate(void * v)=0;
    //some complex field and algorithm

    // **** Addition ****
    // Two objects are considered equal when they are interchangeable at deallocation time.
    // There might be a more refined way to define this relation, but without the internals
    // of Allocator, I'll go with simply being the same object.
    bool operator== (const Allocator & other) const  { return this == &other; }
    bool operator!= (const Allocator & other) const  { return this != &other; }
};

接下来是两个专业。但是,它们的详细信息不在问题范围内。因此,我只是模拟将要编译的东西(之所以需要,是因为不能直接实例化一个抽象基类)。

// Mock-up to allow defining the two allocators.
class DerivedAllocator : public Allocator {
public:
    void * allocate(std::size_t amountByte, int)  override { return std::malloc(amountByte); }
    void   deallocate(void * v)                   override { std::free(v); }
};
DerivedAllocator oneFrameAllocator;
DerivedAllocator heapAllocator;

现在,我们进入第一个大块内容–使Allocator适应标准的期望。它由一个包装器模板组成,该包装器模板的参数是要构造的对象的类型。如果您可以解析Allocator requirements,则此步骤很简单。诚然,解析需求并不简单,因为它们旨在涵盖“花哨的指针”。

// Standard interface for the allocator
template <class T>
struct AllocatorOf {

    // Some basic definitions:

    //Allocator & alloc; // A plain reference is an option if you don't support swapping.
    std::reference_wrapper<Allocator> alloc; // Or a pointer if you want to add null checks.
    AllocatorOf(Allocator & a) : alloc(a) {} // Note: Implicit conversion allowed

    // Maybe this value would come from a helper template? Tough to say, but as long as
    // the value depends solely on T, the value can be a static class constant.
    static constexpr int ALIGN = 0;

    // The things required by the Allocator requirements:

    using value_type = T;
    // Rebind from other types:
    template <class U>
    AllocatorOf(const AllocatorOf<U> & other) : alloc(other.alloc) {}
    // Pass through to Allocator:
    T *  allocate  (std::size_t n)        { return static_cast<T *>(alloc.get().allocate(n * sizeof(T), ALIGN)); }
    void deallocate(T * ptr, std::size_t) { alloc.get().deallocate(ptr); }
    // Support swapping (helps ease writing a constructor)
    using propagate_on_container_swap = std::true_type;
};
// Also need the interchangeability test at this level.
template<class T, class U>
bool operator== (const AllocatorOf<T> & a_t, const AllocatorOf<U> & a_u)
{ return a_t.get().alloc == a_u.get().alloc; }
template<class T, class U>
bool operator!= (const AllocatorOf<T> & a_t, const AllocatorOf<U> & a_u)
{ return a_t.get().alloc != a_u.get().alloc; }

接下来是流形类。最低级别(M1)不需要任何更改。

中级(M2)需要加两个才能获得理想的结果。

  1. 需要定义成员类型allocator_type。它的存在表明该类可识别分配器。
  2. 需要有一个将要复制的对象和要使用的分配器作为参数的构造函数。这使得该类实际上可以识别分配器。 (可能还需要带有分配器参数的其他构造函数,具体取决于您对这些类的实际操作。scoped_allocator的工作方式是将分配器自动附加到提供的构造参数中。由于示例代码在内部复制了副本向量,则需要一个“ copy-plus-allocator”构造函数。)

此外,对于一般用途,中级级别应该获得一个构造函数,其唯一的参数是分配器。为了提高可读性,我还将带回MyArray名称(而不是模板)。

最高级别(M3)仅需要构造函数使用分配器。尽管如此,这两种类型别名对于提高可读性和一致性还是很有用的,因此我也将它们放入。

class M1{};   //e.g. a single-point collision site

class M2{     //e.g. analysed many-point collision site
public:
    using allocator_type = std::scoped_allocator_adaptor<AllocatorOf<M1>>;
    using MyArray        = std::vector<M1, allocator_type>;

    // Default construction still uses oneFrameAllocator, but this can be overridden.
    explicit M2(const allocator_type & alloc = oneFrameAllocator) : m1s(alloc) {}
    // "Copy" constructor used via scoped_allocator_adaptor
    //M2(const M2 & other, const allocator_type & alloc) : m1s(other.m1s, alloc) {}
    // You may want to instead delegate to the true copy constructor. This means that
    // the m1s array will be copied twice (unless the compiler is able to optimize
    // away the first copy). So this would need to be performance tested.
    M2(const M2 & other, const allocator_type & alloc) : M2(other)
    {
        MyArray realloc{other.m1s, alloc};
        m1s.swap(realloc); // This is where we need swap support.
    }

    MyArray m1s;
};

class M3{     //e.g. analysed collision surface
public:
    using allocator_type = std::scoped_allocator_adaptor<AllocatorOf<M2>>;
    using MyArray        = std::vector<M2, allocator_type>;

    // Default construction still uses oneFrameAllocator, but this can be overridden.
    explicit M3(const allocator_type & alloc = oneFrameAllocator) : m2s(alloc) {}

    MyArray m2s;
};

让我们看看...在Allocator中添加了两行(可以减少为一行),在M2中添加了四行,在M3中添加了三行,消除了{{1} }模板,然后添加MyArray模板。差别不大。好吧,如果您想为AllocatorOf利用自动生成的副本构造函数,那还不止于此(但有充分支持向量交换的好处)。总体而言,变化并不大。

以下是代码的使用方式:

M2

此处看到的分配保留了int main() { M3 output_m3{heapAllocator}; for ( int timeStep = 0; timeStep < 100; timeStep++ ) { //v start complex computation #2 M3 m3; M2 m2; M1 m1; m2.m1s.push_back(m1); // <-- vector uses push_back() instead of add() m3.m2s.push_back(m2); // <-- vector uses push_back() instead of add() //^ end complex computation output_m3 = m3; // change to heap allocation //.... clean up oneFrameAllocator here .... } } 的分配策略,因为output_m3并未这样做。这似乎应该是期望的行为,而不是复制分配策略的旧方法。请注意,如果分配的双方已经使用相同的分配策略,则保留或复制该策略都没关系。因此,应保留现有行为,而无需进行进一步更改。

除了指定一个变量使用堆分配之外,使用这些类没有比以前更混乱的了。因为假设在某个时候需要指定堆分配,所以我不明白为什么这会令人反感。使用标准库–可以帮助您。

答案 1 :(得分:5)

由于您的目标是性能,因此我暗示您的类将不会管理分配器本身的生存期,而只会使用它的原始指针。另外,由于您要更改存储空间,因此复制是不可避免的。在这种情况下,您需要为每个类添加一个“参数化副本构造函数”,例如:

template <typename T> class MyArray {
    private:
        Allocator& _allocator;

    public:
        MyArray(Allocator& allocator) : _allocator(allocator) { }
        MyArray(MyArray& other, Allocator& allocator) : MyArray(allocator) {
            // copy items from "other", passing new allocator to their parametrized copy constructors
        }
};

class M1 {
    public:
        M1(Allocator& allocator) { }
        M1(const M1& other, Allocator& allocator) { }
};

class M2 {
    public:
        MyArray<M1> m1s;

    public:
        M2(Allocator& allocator) : m1s(allocator) { }
        M2(const M2& other, Allocator& allocator) : m1s(other.m1s, allocator) { }
};

通过这种方式,您可以轻松做到:

M3 stackM3(stackAllocator);
// do processing
M3 heapM3(stackM3, heapAllocator); // or return M3(stackM3, heapAllocator);

创建基于其他分配器的副本。

此外,根据实际的代码结构,您可以添加一些模板魔术来使事情自动化:

template <typename T> class MX {
    public:
        MyArray<T> ms;

    public:
        MX(Allocator& allocator) : ms(allocator) { }
        MX(const MX& other, Allocator& allocator) : ms(other.ms, allocator) { }
}

class M2 : public MX<M1> {
    public:
        using MX<M1>::MX; // inherit constructors
};

class M3 : public MX<M2> {
    public:
        using MX<M2>::MX; // inherit constructors
};

答案 2 :(得分:3)

我意识到这不是您问题的答案-但是,如果您仅需要下一个周期的对象(而不是该周期之后的将来的周期),您是否可以保留两个一帧分配器在另一个周期中销毁它们?

由于您是自己编写分配器的,因此可以直接在分配器中处理,清理函数可以知道这是一个偶数周期还是奇数周期。

您的代码将如下所示:

int main(){
    M3 output_m3; 
    for(int timeStep=0;timeStep<100;timeStep++){
        oneFrameAllocator.set_to_even(timeStep % 2 == 0);
        //v start complex computation #2
        M3 m3;
        M2 m2;
        M1 m1;
        m2.m1s.add(m1);
        m3.m2s.add(m2);
        //^ end complex computation
        output_m3=m3; 
        oneFrameAllocator.cleanup(timestep % 2 == 1); //cleanup odd cycle
    }
}