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

switching to another different custom allocator -> propagate to member fields

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

这是一个虚拟片段 (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
};

然而,我的单帧分配器有一个缺点——用户必须确保单帧分配器分配的每个对象必须在时间步结束时 deleted/released。

问题

这是一个用例示例。

我使用 one-frame 分配器存储 临时 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-saver)。
因为我只将 M1M2M3 的实例创建为临时变量,所以它有效。

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

换句话说,我想复制一帧分配 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 ....
    }
}

我不能直接分配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. 恕我直言,将分配器类型作为 class 模板参数(例如 MyArray<T,StackAllocator> )传递是不可取的。
  3. 我不介意 Allocator::allocate()Allocator::deallocate() 的 vtable-cost。
  4. 我梦想有一个 C++ pattern/tool 可以自动将分配器传播给 class 的成员。也许,它是 MSalters 建议的 operator=(),但我找不到合适的方法来实现它。

参考:收到JaMiT的回答后,发现这个问题是类似于 .

既然你的目标是性能,我的意思是你的 classes 不会管理分配器本身的生命周期,而只会使用它的原始指针。此外,由于您正在更改存储,因此复制是不可避免的。在这种情况下,您只需要为每个 class 添加一个 "parametrized copy constructor",例如:

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

我知道这不是你问题的答案——但如果你只需要下一个周期的对象(而不是之后的未来周期),你能不能只保留两个单帧分配器交替销毁它们周期?

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

您的代码将类似于:

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

理由

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

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

  • One-frame 分配要求 object 在 timeStep.
  • 循环结束时销毁
  • 当 one-frame 无法分配时,应使用堆分配。

也就是说,您可以通过查看相关 object/variable 的范围来判断使用哪种分配策略。 (它是在循环内部还是外部body?)作用域在构造时是已知的并且不会改变(只要你不滥用std::move)。所以所需的分配器在构建时是已知的并且不会改变。但是,当前的构造函数不允许指定分配器。这是需要改变的。幸运的是,这种变化是引入 scoped_allocator_adaptor.

的相当自然的延伸

另一大变化是折腾MyArrayclass。存在标准容器以使您的编程更容易。与编写您自己的版本相比,标准容器实施起来更快(已经完成)并且更不容易出错(该标准力求比 "works for me this time" 更高的质量标准)。因此,使用 MyArray 模板并使用 std::vector.

怎么做

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

您当前的 Allocator class 是一个合理的起点。它只需要一对方法来指示两个实例何时可以互换(即,当两个实例都能够释放由它们中的任何一个分配的内存时)。我还冒昧地将 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; }
};

接下来是两个专业。不过,他们的详细信息超出了问题的范围。所以我只是模拟一些可以编译的东西(需要,因为不能直接实例化一个抽象基础class)。

// 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 适应标准的期望。这由一个包装器模板组成,其参数是正在构造的 object 的类型。如果你能解析Allocator requirements,这一步就简单了。诚然,解析需求并不简单,因为它们旨在涵盖 "fancy pointers".

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

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

mid-levels (M2) 需要两次相加才能得到想要的结果。

  1. 需要定义成员类型allocator_type。它的存在表明 class 是 allocator-aware.
  2. 需要有一个构造函数,该构造函数将要复制的 object 和要使用的分配器作为参数。这使得 class 实际上是 allocator-aware。 (可能需要其他带有分配器参数的构造函数,具体取决于您实际对这些 classes 执行的操作。scoped_allocator 通过自动将分配器附加到提供的构造参数来工作。由于示例代码在向量内部进行复制,因此需要一个 "copy-plus-allocator" 构造函数。)

此外,对于一般用途,mid-levels 应该得到一个构造函数,其唯一参数是一个分配器。为了便于阅读,我还将带回 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(可以减少到只有一行),将 four-ish 添加到 M2,将三行添加到 M3,消除MyArray 模板,并添加 AllocatorOf 模板。这不是很大的区别。好吧,如果你想利用 M2 的 auto-generated 复制构造函数(但具有完全支持向量交换的好处),那么数量就多了一点。总的来说,变化不大。

代码的使用方式如下:

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没有说要不然。这似乎应该是期望的行为,而不是旧的复制分配策略的方法。请注意,如果分配的双方已经使用相同的分配策略,则保留或复制该策略都没有关系。因此,应保留现有行为,无需进一步更改。

除了指定一个变量使用堆分配外,classes 的使用并不比以前更混乱。由于假定在某些时候需要指定堆分配,所以我不明白为什么这会是 objectionable。使用标准库 – 它可以提供帮助。