C++ 批处理分配库

C++ 批处理分配库,c++,dynamic-memory-allocation,applicative,C++,Dynamic Memory Allocation,Applicative,因此,我目前正在重构一个巨大的函数: int giant_function(size_t n, size_t m, /*... other parameters */) { int x[n]{}; float y[n]{}; int z[m]{}; /* ... more array definitions */ 当我找到一组具有离散功能的相关定义时,将它们分组为类定义: class V0 { std::unique_ptr<int[]> x; std::u

因此,我目前正在重构一个巨大的函数:

int giant_function(size_t n, size_t m, /*... other parameters */) {
  int x[n]{};
  float y[n]{};
  int z[m]{};
  /* ... more array definitions */
当我找到一组具有离散功能的相关定义时,将它们分组为类定义:

class V0 {
  std::unique_ptr<int[]> x;
  std::unique_ptr<float[]> y;
  std::unique_ptr<int[]> z;
public:
  V0(size_t n, size_t m)
    : x{new int[n]{}}
    , y{new float[n]{}}
    , z{new int[m]{}}
  {}
  // methods...
}
这种方法不仅需要大量的手工(阅读:容易出错)记账,而且更大的缺点是它不可组合

如果我想在堆栈上有一个
V1
值和一个
W1
值,那就是 每个人分配一个幕后资源。更简单的是,我希望能够在一次分配中分配
V1
和它所指向的资源,而我不能用这种方法

这使我最初采用了两步方法——一步计算需要多少空间,然后进行一次巨大的分配,然后再进行一次分配并初始化数据结构

class V2 {
  int* x;
  float* y;
  int* z;
public:
  static size_t size(size_t n, size_t m) {
    return sizeof(V2) + n*sizeof(int) + n*sizeof(float) + m*sizeof(int);
  }
  V2(size_t n, size_t m, char** buf) {
    x = (int*) *buf;
    *buf += n*sizeof(int);
    y = (float*) *buf;
    *buf += n*sizeof(float);
    z = (int*) *buf;
    *buf += m*sizeof(int);
  }
}  
// ...
size_t total = ... + V2::size(n,m) + ...
char* buf = new char[total];
// ...
void*  here = buf;
buf += sizeof(V2);
V2* v2 = new (here) V2{n, m, &buf};
然而,这种方法在远处重复了很多次,从长远来看,这会带来麻烦。退回一家工厂后,我们就摆脱了:

class V3 {
  int* const x;
  float* const y;
  int* const z;
  V3(int* x, float* y, int* z) : x{x}, y{y}, z{z} {}
public:
  class V3Factory {
    size_t const n;
    size_t const m;
  public:
    Factory(size_t n, size_t m) : n{n}, m{m};
    size_t size() {
      return sizeof(V3) + sizeof(int)*n + sizeof(float)*n + sizeof(int)*m;
    }
    V3* build(char** buf) {
      void * here = *buf;
      *buf += sizeof(V3);
      x = (int*) *buf;
      *buf += n*sizeof(int);
      y = (float*) *buf;
      *buf += n*sizeof(float);
      z = (int*) *buf;
      *buf += m*sizeof(int);
      return new (here) V3{x,y,z};
    }
  }
}
// ...
V3::Factory v3factory{n,m};
// ...
size_t total = ... + v3factory.size() + ...
char* buf = new char[total];
// ..
V3* v3 = v3factory.build(&buf);
仍然有一些重复,但是参数只得到一次输入。还有很多手工记账。如果我能用更小的工厂来建造这家工厂那就太好了

然后我的哈斯凯尔脑撞到了我。我正在实现一个应用函数。这完全可以更好

我所需要做的就是编写一些工具来自动计算大小并并行运行构建函数:

namespace plan {

template <typename A, typename B>
struct Apply {
  A const a;
  B const b;
  Apply(A const a, B const b) : a{a}, b{b} {};

  template<typename ... Args>
  auto build(char* buf, Args ... args) const {
    return a.build(buf, b.build(buf + a.size()), args...);
  }

  size_t size() const {
    return a.size() + b.size();
  }

  Apply(Apply<A,B> const & plan) : a{plan.a}, b{plan.b} {}
  Apply(Apply<A,B> const && plan) : a{plan.a}, b{plan.b} {}

  template<typename U, typename ... Vs>
  auto operator()(U const u, Vs const ... vs) const {
    return Apply<decltype(*this),U>{*this,u}(vs...);
  }

  auto operator()() const {
    return *this;
  }
};
template<typename T>
struct Lift {
  template<typename ... Args>
  T* build(char* buf, Args ... args) const {
    return new (buf) T{args...};
  }
  size_t size() const {
    return sizeof(T);
  }
  Lift() {}
  Lift(Lift<T> const &) {}
  Lift(Lift<T> const &&) {}

  template<typename U, typename ... Vs>
  auto operator()(U const u, Vs const ... vs) const {
    return Apply<decltype(*this),U>{*this,u}(vs...);
  }

  auto operator()() const {
    return *this;
  }
}; 

template<typename T>
struct Array {
  size_t const length;
  Array(size_t length) : length{length} {}
  T* build(char* buf) const {
    return new (buf) T[length]{};
  }
  size_t size() const {
    return sizeof(T[length]);
  }
};

template <typename P>
auto heap_allocate(P plan) {
  return plan.build(new char[plan.size()]);
}

}
名称空间计划{
模板
结构应用{
常数A;
B常数B;
应用(常数A,常数B):A{A},B{B};
模板
自动生成(字符*buf,参数…参数)常量{
返回a.build(buf,b.build(buf+a.size()),args;
}
大小\u t大小()常量{
返回a.size()+b.size();
}
Apply(Apply const&plan):a{plan.a},b{plan.b}{
Apply(Apply const&&plan):a{plan.a},b{plan.b}{
模板
自动运算符()(U常量U,Vs常量…Vs)常量{
返回应用{*this,u}(vs.);
}
自动运算符()()常量{
归还*这个;
}
};
模板
结构升降机{
模板
T*构建(char*buf,Args…Args)常量{
返回新的(buf)T{args…};
}
大小\u t大小()常量{
返回大小f(T);
}
提升(){}
提升(提升常数&){}
提升(提升常数&&{}
模板
自动运算符()(U常量U,Vs常量…Vs)常量{
返回应用{*this,u}(vs.);
}
自动运算符()()常量{
归还*这个;
}
}; 
模板
结构数组{
尺寸常数长度;
数组(大小\u t长度):长度{length}{}
T*构建(char*buf)常量{
返回新的(buf)T[length]{};
}
大小\u t大小()常量{
返回sizeof(T[长度]);
}
};
模板
自动堆分配(P计划){
return plan.build(新字符[plan.size()]);
}
}
现在,我可以简单地说明我的课程:

class V4 {
  int* const x;
  float* const y;
  int* const z;

public:
  V4(int* x, float* y, int* z) : x{x}, y{y}, z{z} {}

  static auto plan(size_t n, size_t m) {
    return plan::Lift<V4>{}(
      plan::Array<int>{n},
      plan::Array<float>{n},
      plan::Array<int>{m}
    );
  }
};
V4类{
int*常数x;
浮动*常数;
int*常数z;
公众:
V4(int*x,float*y,int*z):x{x},y{y},z{z}
静态自动计划(大小n,大小m){
返回计划::提升{}(
计划::数组{n},
计划::数组{n},
计划::数组{m}
);
}
};
并一次性使用:

V4* v4;
W4* w4;
std::tie{ ..., v4, w4, .... } = *plan::heap_allocate(
  plan::Lift<std::tie>{}(
    // ...
    V4::plan(n,m),
    W4::plan(m,p,2*m+1),
    // ...
  )
);
V4*V4;
W4*W4;
std::tie{…,v4,w4,…}=*计划::堆分配(
计划::提升{}(
// ...
V4::平面图(n,m),
W4::平面图(m,p,2*m+1),
// ...
)
);
它并不完美(在其他问题中,我需要添加代码来跟踪析构函数,并让
heap\u allocate
返回一个调用所有函数的
std::unique\u ptr
),但在我进一步深入兔子洞之前,我认为我应该检查一下已有的art

据我所知,现代编译器可能足够聪明,可以认识到
V0
中的内存总是一起分配/解除分配,并为我批处理分配


如果没有,这个想法(或其变体)是否已有实现,用于使用applicative functor批处理分配?

首先,我想就您的解决方案的问题提供反馈:

  • 您忽略对齐。假设
    int
    float
    在您的系统上共享相同的对齐方式,您的特定用例可能是“好的”。但是试着在混合中加入一些
    double
    ,就会出现UB。您可能会发现您的程序由于未对齐的访问而在ARM芯片上崩溃

  • new(buf)T[length]{}是错误的。简而言之:标准允许编译器保留给定存储的初始
    y
    字节供内部使用。您的程序无法在
    y>0
    的系统上分配这个
    y
    字节(是的,这些系统显然存在;VC++据称是这样做的)

    必须为
    y
    进行分配是不好的,但使数组placement new无法使用的是,在实际调用placement new之前,无法确定
    y
    有多大。真的没有办法用它来处理这个案子

  • 您已经意识到了这一点,但为了完整性:您不会破坏子缓冲区,因此如果您使用非平凡的可破坏类型,那么将有UB


  • 解决方案:

  • 为每个缓冲区分配额外的
    alignof(T)-1
    字节。将每个缓冲区的开始与
    std::Align
    对齐

  • 您需要循环并使用非数组放置。从技术上讲,在这些对象上使用非数组放置新方法意味着使用指针算法有UB,但标准在这方面很愚蠢,我选择忽略它。语言律师对此的讨论。据我所知,该提案包括一项关于这一技术性问题的决议

  • 添加与放置新调用相对应的析构函数调用(或只应使用微不足道的可破坏类型的
    static_assert
    )。注意,对非平凡破坏的支持增加了对异常安全的需求。如果一个缓冲区的构造引发异常,则需要销毁先前构造的子缓冲区。当建造单个
    V4* v4;
    W4* w4;
    std::tie{ ..., v4, w4, .... } = *plan::heap_allocate(
      plan::Lift<std::tie>{}(
        // ...
        V4::plan(n,m),
        W4::plan(m,p,2*m+1),
        // ...
      )
    );
    
    #include <cstddef>
    #include <memory>
    #include <vector>
    #include <tuple>
    #include <cassert>
    #include <type_traits>
    #include <utility>
    
    // recursion base
    template <class... Args>
    class buffer_clump {
    protected:
        constexpr std::size_t buffer_size() const noexcept { return 0; }
        constexpr std::tuple<> buffers(char*) const noexcept { return {}; }
        constexpr void construct(char*) const noexcept { }
        constexpr void destroy(const char*) const noexcept {}
    };
    
    template<class Head, class... Tail>
    class buffer_clump<Head, Tail...> : buffer_clump<Tail...> {
        using tail = buffer_clump<Tail...>;
        const std::size_t length;
        
        constexpr std::size_t size() const noexcept
        {
            return sizeof(Head) * length + alignof(Head) - 1;
        }
        
        constexpr Head* align(char* buf) const noexcept
        {
            void* aligned = buf;
            std::size_t space = size();
            assert(std::align(
                alignof(Head),
                sizeof(Head) * length,
                aligned,
                space
            ));
            return (Head*)aligned;
        }
        
        constexpr char* next(char* buf) const noexcept
        {
            return buf + size();
        }
        
        static constexpr void
        destroy_head(Head* head_ptr, std::size_t last)
        noexcept(std::is_nothrow_destructible<Head>::value)
        {
            if constexpr (!std::is_trivially_destructible<Head>::value)
                while (last--)
                    head_ptr[last].~Head();
        }
        
    public:
        template<class... Size_t>
        constexpr buffer_clump(std::size_t length, Size_t... tail_lengths) noexcept
        : tail(tail_lengths...), length(length) {}
        
        constexpr std::size_t
        buffer_size() const noexcept
        {
            return size() + tail::buffer_size();
        }
        
        constexpr auto
        buffers(char* buf) const noexcept
        {
            return std::tuple_cat(
                std::make_tuple(align(buf)), 
                tail::buffers(next(buf))
            );
        }
        
        void
        construct(char* buf) const
        noexcept(std::is_nothrow_default_constructible<Head, Tail...>::value)
        {
            Head* aligned = align(buf);
            std::size_t i;
            try {
                for (i = 0; i < length; i++)
                    new (&aligned[i]) Head;
                tail::construct(next(buf));
            } catch (...) {
                destroy_head(aligned, i);
                throw;
            }
        }
        
        constexpr void
        destroy(char* buf) const
        noexcept(std::is_nothrow_destructible<Head, Tail...>::value)
        {
            tail::destroy(next(buf));
            destroy_head(align(buf), length);
        }
    };
    
    template <class... Args>
    class buffer_clump_storage {
        const buffer_clump<Args...> clump;
        std::vector<char> storage;
        
    public:
        constexpr auto buffers() noexcept {
            return clump.buffers(storage.data());
        }
        
        template<class... Size_t>
        buffer_clump_storage(Size_t... lengths)
        : clump(lengths...), storage(clump.buffer_size())
        {
            clump.construct(storage.data());
        }
        
        ~buffer_clump_storage()
        noexcept(noexcept(clump.destroy(nullptr)))
        {
            if (storage.size())
                clump.destroy(storage.data());
        }
    
        buffer_clump_storage(buffer_clump_storage&& other) noexcept
        : clump(other.clump), storage(std::move(other.storage))
        {
            other.storage.clear();
        }
    };
    
    class V5 {
        // macro tricks or boost mpl magic could be used to avoid repetitive boilerplate
        buffer_clump_storage<int, float, int> storage;
        
    public:
        int* x;
        float* y;
        int* z;
        V5(std::size_t xs, std::size_t  ys, std::size_t zs)
        : storage(xs, ys, zs)
        {
            std::tie(x, y, z) = storage.buffers();
        }
    };
    
    int giant_function(size_t n, size_t m, /*... other parameters */) {
        V5 v(n, n, m);
        for(std::size_t i = 0; i < n; i++)
            v.x[i] = i;
    
    int giant_function(size_t n, size_t m, /*... other parameters */) {
        buffer_clump_storage<int, float, int> v(n, n, m);
        auto [x, y, z] = v.buffers();