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

问题描述 投票:6回答:3

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

这是一个虚拟片段(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. 恕我直言,将类型分配器作为类模板参数(例如MyArray<T,StackAllocator>)传递是不合需要的。
  3. 我不介意Allocator::allocate()Allocator::deallocate()的vtable成本。
  4. 我梦想有一个C ++模式/工具可以自动将分配器传播给类的成员。也许,像MSalters建议的那样是operator=(),但我找不到合适的方法来实现它。

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

c++ class memory c++14 allocator
3个回答
8
投票

理由

从本质上讲,这个问题是要求一种方法来使用具有多级容器的自定义分配器。还有其他规定,但在考虑了这个之后,我决定忽略其中的一些规定。他们似乎没有充分理由阻碍解决方案。这留下了标准库答案的可能性:std::scoped_allocator_adaptorstd::vector

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

  • 一帧分配要求在timeStep循环结束时销毁对象。
  • 当一帧分配不能时,应该使用堆分配。

也就是说,您可以通过查看相关对象/变量的范围来确定要使用的分配策略。 (它是在循环体的内部还是外部?)范围在构造时已知并且不会改变(只要您不滥用std::move)。因此,期望的分配器在构造时已知并且不会改变。但是,当前构造函数不允许指定分配器。这是要改变的事情。幸运的是,这种变化是引入scoped_allocator_adaptor的相当自然的延伸。

另一个重大变化是扔MyArray级。存在标准容器以使您的编程更容易。与编写自己的版本相比,标准容器的实现速度更快(如已经完成)并且不易出错(标准的质量标准比“这次为我工作”更高)。所以用MyArray模板和std::vector

怎么做

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

您目前的Allocator课程是一个合理的起点。它只需要一对方法来指示两个实例何时可以互换(即两者都能够释放由其中任何一个分配的内存)。我也冒昧地将amountByte更改为无符号类型,因为分配负数量的内存是没有意义的。 (虽然我只留下了align的类型,因为没有迹象表明这将采取什么价值。可能它应该是unsigned或枚举。)

#include <cstdlib>
#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; // Or a pointer if you really want to add null checks.
    AllocatorOf(Allocator & a) : alloc(a) {} // Note: Implicit conversion

    // 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.allocate(n * sizeof(T), ALIGN)); }
    void deallocate(T * ptr, std::size_t) { alloc.deallocate(ptr); }
};
// 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.alloc == a_u.alloc; }
template<class T, class U>
bool operator!= (const AllocatorOf<T> & a_t, const AllocatorOf<U> & a_u)
{ return a_t.alloc != a_u.alloc; }

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

中级(M2)需要两次添加才能获得所需的结果。

  1. 需要定义成员类型allocator_type。它的存在表明该类是分配器感知的。
  2. 需要有一个构造函数,它将要复制的对象和要使用的分配器作为参数。这使得类实际上可以识别分配器。 (可能需要具有allocator参数的其他构造函数,具体取决于您对这些类的实际操作.scoped_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) {}

    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,消除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的分配策略,因为AllocatorOf没有说不这样做。这似乎应该是理想的行为,而不是复制分配策略的旧方法。请注意,如果赋值的两端已使用相同的分配策略,则保留或复制策略无关紧要。因此,应保留现有行为而无需进一步更改。

除了指定一个变量使用堆分配之外,使用类并不比以前更混乱。由于假设在某些时候需要指定堆分配,我不明白为什么这会令人反感。使用标准库 - 它可以提供帮助。


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
};

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
    }
}