Skip to content

Instantly share code, notes, and snippets.

@etorth
Created June 13, 2020 02:06
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save etorth/e14e955d0183ef606dffb3a2d42030c9 to your computer and use it in GitHub Desktop.
Save etorth/e14e955d0183ef606dffb3a2d42030c9 to your computer and use it in GitHub Desktop.
code review for allocator implementation
#pragma once
#include <cstdio>
#include <cstdlib>
#include <cstddef>
#include <cassert>
#include <vector>
#include <stdexcept>
#include "fflerror.h" // macro, same as std::runtime_error, but with file/function/line information
// #define SCOPED_ALLOC_USE_THROW
namespace scoped_alloc
{
constexpr bool check_alignment(size_t alignment)
{
return alignment && (alignment & (alignment - 1)) == 0;
}
template<size_t Alignment> struct aligned_buf
{
constexpr static size_t alignment = Alignment;
static_assert(check_alignment(Alignment), "bad alignment");
const char *buf = nullptr;
const size_t size = 0;
};
template<size_t Alignment = alignof(std::max_align_t)> class arena_interf
{
private:
static_assert(check_alignment(Alignment), "bad alignment");
protected:
char *m_cursor = nullptr;
public:
virtual aligned_buf<Alignment> get_buf() const = 0;
public:
arena_interf() = default;
public:
arena_interf(arena_interf &&) = delete;
arena_interf(const arena_interf&) = delete;
arena_interf &operator = (const arena_interf &) = delete;
public:
virtual ~arena_interf()
{
m_cursor = nullptr;
}
protected:
bool in_buf(char *p) const
{
const auto buf = get_buf();
if(!(buf.buf && buf.size)){
throw fflerror("no valid buffer attached to arena_interf");
}
return buf.buf <= p && p <= buf.buf + buf.size;
}
protected:
static size_t aligned_size(size_t byte_count) noexcept
{
return (byte_count + (Alignment - 1)) & ~(Alignment - 1);
}
public:
template<size_t RequestAlignment> char *allocate(size_t byte_count)
{
static_assert(RequestAlignment <= Alignment, "alignment too small");
detect_outlive();
const auto buf = get_buf();
const auto byte_count_aligned = aligned_size(byte_count);
if(static_cast<decltype(byte_count_aligned)>(buf.buf + buf.size - m_cursor) >= byte_count_aligned){
auto r = m_cursor;
m_cursor += byte_count_aligned;
return r;
}
// TODO normal operator new() only guarantees alignemnt <= alignof(std::max_align_t)
// but class user may ask for over-aligned memory, how to handle this?
//
// 1. raise compilation error if asks for over-alignment
// 2. here I try posix_memalign(), this may waste memory, or other issue?
//
return dynamic_alloc(byte_count);
}
public:
void deallocate(char *p, size_t byte_count) noexcept
{
detect_outlive();
if(in_buf(p)){
if(p + aligned_size(byte_count) == m_cursor){
m_cursor = p;
}
else{
// TODO: main draw-back
// can't recycle memory here without extra flags
}
}
else{
free(p); // allocated by posix_memalign()
}
}
public:
virtual char *dynamic_alloc(size_t byte_count)
{
return aligned_alloc(byte_count);
}
public:
static char *aligned_alloc(size_t byte_count)
{
void *aligned_ptr = nullptr;
if(!posix_memalign(&aligned_ptr, Alignment, byte_count)){
return static_cast<char *>(aligned_ptr);
}
throw fflerror("posix_memalign(..., alignment = %zu, byte_count = %zu) failed", Alignment, byte_count);
}
private:
void detect_outlive() const
{
// TODO: best-effort
// detect_outlive() invokes undefined behavior if assertion failed
#ifdef SCOPED_ALLOC_USE_THROW
if(!(in_buf(m_cursor))){
throw fflerror("allocator has outlived arena_interf");
}
#else
assert(in_buf(m_cursor) && "allocator has outlived arena_interf");
#endif
}
};
template<size_t ByteCount, size_t Alignment = alignof(std::max_align_t)> class fixed_arena: public scoped_alloc::arena_interf<Alignment>
{
private:
alignas(Alignment) char m_buf[ByteCount];
public:
fixed_arena(): scoped_alloc::arena_interf<Alignment>()
{
this->m_cursor = m_buf;
}
public:
aligned_buf<Alignment> get_buf() const override
{
return {m_buf, ByteCount};
}
};
template<size_t Alignment = alignof(std::max_align_t)> class dynamic_arena: public scoped_alloc::arena_interf<Alignment>
{
private:
char *m_buf = nullptr;
size_t m_len = 0;
public:
dynamic_arena(size_t byte_count = 0): scoped_alloc::arena_interf<Alignment>()
{
if(!byte_count){
return;
}
alloc(byte_count);
}
dynamic_arena(const scoped_alloc::aligned_buf<Alignment> &buf)
: scoped_alloc::arena_interf<Alignment>()
, m_buf(buf.buf)
, m_len(buf.size)
{
if(!(m_buf && m_len)){
throw fflerror("invalid aligned_buf<%zu>(buf = %p, size = %zu)", Alignment, m_buf, m_len);
}
this->m_cursor = m_buf;
}
public:
~dynamic_arena() override
{
if(m_buf){
free(m_buf);
}
m_buf = nullptr;
m_len = 0;
}
public:
aligned_buf<Alignment> get_buf() const override
{
return {m_buf, m_len};
}
public:
void alloc(size_t byte_count)
{
if(auto new_buf = scoped_alloc::arena_interf<Alignment>::aligned_alloc(byte_count)){
if(m_buf){
free(m_buf);
}
m_buf = new_buf;
m_len = byte_count;
this->m_cursor = new_buf;
}
}
public:
aligned_buf<Alignment> steal_buf()
{
auto buf = m_buf;
auto len = m_len;
m_buf = nullptr;
m_len = 0;
this->m_cursor = nullptr;
return {buf, len};
}
};
template<class T, size_t Alignment = alignof(std::max_align_t)> class allocator
{
public:
using value_type = T;
template <class U, size_t A> friend class scoped_alloc::allocator;
public:
template <class UpperType> struct rebind
{
using other = allocator<UpperType, Alignment>;
};
private:
arena_interf<Alignment> &m_arena;
public:
allocator(const allocator &) = default;
allocator &operator=(const allocator &) = delete;
public:
allocator(arena_interf<Alignment> &arena_ref) noexcept
: m_arena(arena_ref)
{}
public:
template<class U> allocator(const allocator<U, Alignment>& alloc_ref) noexcept
: m_arena(alloc_ref.m_arena)
{}
public:
T* allocate(size_t n)
{
return reinterpret_cast<T *>(m_arena.template allocate<alignof(T)>(n * sizeof(T)));
}
void deallocate(T *p, size_t n) noexcept
{
m_arena.deallocate(reinterpret_cast<char *>(p), n * sizeof(T));
}
public:
template<typename T2, size_t A2> bool operator == (const scoped_alloc::allocator<T2, A2> &parm) noexcept
{
return Alignment == A2 && &(this->m_arena) == &(parm.m_arena);
}
template<typename T2, size_t A2> bool operator != (const scoped_alloc::allocator<T2, A2> &parm) noexcept
{
return !(*this == parm);
}
};
}
template<typename T, size_t N> class svobuf
{
private:
scoped_alloc::fixed_arena<N * sizeof(T), alignof(T)> m_arena;
public:
std::vector<T, scoped_alloc::allocator<T, alignof(T)>> c;
public:
svobuf()
: c(typename decltype(c)::allocator_type(m_arena))
{
// immediately use all static buffer
// 1. to prevent push_back() to allocate on heap, best effort
// 2. to prevent waste of memory
//
// svobuf<int, 4> buf;
// auto buf_cp = buf.c;
//
// buf_cp.push_back(1);
// buf_cp.push_back(2);
//
// here buf has ran out of all static buffer
// the copy constructed buf_cp will always allocates memory on heap
//
// ill-formed code:
//
// auto f()
// {
// svobuf<int, 4> buf;
//
// buf.c.push_back(0);
// buf.c.push_back(1);
// ...
//
// return buf.c;
// }
//
// return a copy of buf.c also copies its scoped allocator
// this causes dangling reference
c.reserve(N);
if(c.capacity() > N){
throw fflerror("allocate initial buffer dynamically");
}
}
public:
constexpr size_t fixed_capacity() const
{
return N;
}
};
@etorth
Copy link
Author

etorth commented Jun 13, 2020

test program:

#include <cstdlib>
#include <iostream>
#include <list>
#include <map>
#include <cstring>
#include "scopedalloc.h"

int main()
{
    svobuf<int, 12> buf;

    buf.c.push_back(0);
    buf.c.push_back(1);
    buf.c.push_back(2);

    scoped_alloc::dynamic_arena<> f;
    std::vector<std::vector<int, scoped_alloc::allocator<int>>> vec;
    vec.resize(12, typename decltype(vec)::value_type(f));


    f.alloc(12800);
    vec.back().push_back(0);

    std::list<std::vector<int, scoped_alloc::allocator<int>>> lis;
    lis.push_back(typename decltype(vec)::value_type(f));
    lis.push_back(typename decltype(vec)::value_type(f));
    lis.push_back(typename decltype(vec)::value_type(f));
    lis.push_back(typename decltype(vec)::value_type(f));
    lis.push_back(typename decltype(vec)::value_type(f));

    lis.pop_back();
    lis.pop_back();
    lis.pop_back();

    lis.front().push_back(12);

    using overalign = std::aligned_storage_t<128, 32>;

    overalign t;
    std::memset(&t, 0, sizeof(t));
    std::cout << alignof(t) << std::endl;

    scoped_alloc::dynamic_arena<32> overf;
    std::vector<overalign, scoped_alloc::allocator<overalign, alignof(overalign)>> ovec(overf);

    overf.alloc(1024);

    ovec.push_back(t);
    ovec.push_back({});
    ovec.push_back({});

    std::map<int, int, std::less<int>, scoped_alloc::allocator<overalign, alignof(overalign)>> tmap(overf);
    tmap[12] = 23;

    scoped_alloc::fixed_arena<1024, 32> fixedf;
    std::map<int, int, std::less<int>, scoped_alloc::allocator<overalign, alignof(overalign)>> fixedmap(fixedf);
    fixedmap.insert({2, 10});


    return 0;
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment