Reworked Zone memory allocator a bit.

This commit is contained in:
kobalicek
2014-05-29 14:25:40 +02:00
parent 6def28a509
commit 5869dc4736
6 changed files with 208 additions and 152 deletions

View File

@@ -29,7 +29,7 @@ CodeGen::CodeGen(Runtime* runtime) :
_features(static_cast<uint8_t>(IntUtil::mask(kCodeGenOptimizedAlign))),
_error(kErrorOk),
_options(0),
_baseZone(16384 - sizeof(Zone::Chunk) - kMemAllocOverhead) {}
_baseZone(16384 - kZoneOverhead) {}
CodeGen::~CodeGen() {
if (_errorHandler != NULL)

View File

@@ -44,9 +44,9 @@ BaseCompiler::BaseCompiler(Runtime* runtime) :
_lastNode(NULL),
_cursor(NULL),
_func(NULL),
_varZone(4096 - kMemAllocOverhead),
_stringZone(4096 - kMemAllocOverhead),
_localConstZone(4096 - kMemAllocOverhead),
_varZone(4096 - kZoneOverhead),
_stringZone(4096 - kZoneOverhead),
_localConstZone(4096 - kZoneOverhead),
_localConstPool(&_localConstZone),
_globalConstPool(&_baseZone) {}

View File

@@ -22,7 +22,7 @@ namespace asmjit {
BaseContext::BaseContext(BaseCompiler* compiler) :
_compiler(compiler),
_baseZone(8192 - sizeof(Zone::Chunk) - kMemAllocOverhead) {
_baseZone(8192 - kZoneOverhead) {
BaseContext::reset();
}

View File

@@ -29,32 +29,39 @@
// efficient. There are several goals I decided to write implementation myself.
//
// Goals:
// - We need usually to allocate blocks of 64 bytes long and more.
// - Alignment of allocated blocks is large - 32 bytes or 64 bytes.
// - Keep memory manager information outside allocated virtual memory pages
// (these pages allows execution of code).
// - Keep implementation small.
//
// I think that implementation is not small and probably not too much readable,
// so there is small know how.
// - Granularity of allocated blocks is different than granularity for a typical
// C malloc. It is at least 64-bytes so Assembler/Compiler can guarantee the
// alignment required. Alignment requirements can grow in the future, but at
// the moment 64 bytes is safe (we may jump to 128 bytes if necessary or make
// it configurable).
//
// - Implementation is based on bit arrays and binary trees. Bit arrays
// contains information about allocated and unused blocks of memory. Each
// block size describes MemNode::density member. Count of blocks are
// stored in MemNode::blocks member. For example if density is 64 and
// count of blocks is 20, memory node contains 64*20 bytes of memory and
// smallest possible allocation (and also alignment) is 64 bytes. So density
// describes also memory alignment. Binary trees are used to enable fast
// lookup into all addresses allocated by memory manager instance. This is
// used mainly in VMemPrivate::release().
// - Keep memory manager information outside of the allocated virtual memory
// pages, because these pages allow executing of machine code and there should
// not be data required to keep track of these blocks. Another reason is that
// some environments (i.e. iOS) allow to generate and run JIT code, but this
// code has to be set to [Executable, but not Writable].
//
// - Keep implementation simple and easy to follow.
//
// Implementation is based on bit arrays and binary trees. Bit arrays contain
// information related to allocated and unused blocks of memory. The size of
// a block is described by `MemNode::density`. Count of blocks is stored in
// `MemNode::blocks`. For example if density is 64 and count of blocks is 20,
// memory node contains 64*20 bytes of memory and smallest possible allocation
// (and also alignment) is 64 bytes. So density is also related to memory
// alignment. Binary trees (RB) are used to enable fast lookup into all addresses
// allocated by memory manager instance. This is used mainly by `VMemPrivate::release()`.
//
// Bit array looks like this (empty = unused, X = used) - Size of block 64:
//
// Bit array looks like this (empty = unused, X = used) - Size of block 64
// -------------------------------------------------------------------------
// | |X|X| | | | | |X|X|X|X|X|X| | | | | | | | | | | | |X| | | | |X|X|X| | |
// -------------------------------------------------------------------------
// Bits array shows that there are 12 allocated blocks of 64 bytes, so total
// allocated size is 768 bytes. Maximum count of continuous blocks is 12
// (see largest gap).
// (Maximum continuous block)
//
// These bits show that there are 12 allocated blocks (X) of 64 bytes, so total
// size allocated is 768 bytes. Maximum count of continuous memory is 12 * 64.
namespace asmjit {

View File

@@ -19,18 +19,18 @@
namespace asmjit {
//! Zero width chunk used when Zone doesn't have any memory allocated.
static const Zone::Chunk Zone_zeroChunk = {
NULL, 0, 0, { 0 }
//! Zero size block used by `Zone` that doesn't have any memory allocated.
static const Zone::Block Zone_zeroBlock = {
NULL, NULL, NULL, NULL, { 0 }
};
// ============================================================================
// [asmjit::Zone - Construction / Destruction]
// ============================================================================
Zone::Zone(size_t chunkSize) {
_chunks = const_cast<Zone::Chunk*>(&Zone_zeroChunk);
_chunkSize = chunkSize;
Zone::Zone(size_t blockSize) {
_blocks = const_cast<Zone::Block*>(&Zone_zeroBlock);
_blockSize = blockSize;
}
Zone::~Zone() {
@@ -42,35 +42,33 @@ Zone::~Zone() {
// ============================================================================
void Zone::clear() {
Chunk* cur = _chunks;
Block* cur = _blocks;
if (cur == &Zone_zeroChunk)
// Can't be altered.
if (cur == &Zone_zeroBlock)
return;
while (cur->prev != NULL)
cur = cur->prev;
while (cur != NULL) {
Chunk* prev = cur->prev;
::free(cur);
cur = prev;
}
_chunks->pos = 0;
_chunks->prev = NULL;
cur->pos = cur->data;
_blocks = cur;
}
void Zone::reset() {
Chunk* cur = _chunks;
Block* cur = _blocks;
if (cur == &Zone_zeroChunk)
// Can't be altered.
if (cur == &Zone_zeroBlock)
return;
while (cur != NULL) {
Chunk* prev = cur->prev;
do {
Block* prev = cur->prev;
::free(cur);
cur = prev;
}
} while (cur != NULL);
_chunks = const_cast<Zone::Chunk*>(&Zone_zeroChunk);
_blocks = const_cast<Zone::Block*>(&Zone_zeroBlock);
}
// ============================================================================
@@ -78,38 +76,58 @@ void Zone::reset() {
// ============================================================================
void* Zone::_alloc(size_t size) {
Chunk* cur = _chunks;
ASMJIT_ASSERT(cur == &Zone_zeroChunk || cur->getRemainingSize() < size);
Block* curBlock = _blocks;
size_t blockSize = IntUtil::iMax<size_t>(_blockSize, size);
size_t chunkSize = _chunkSize;
if (chunkSize < size)
chunkSize = size;
// The `_alloc()` method can only be called if there is not enough space
// in the current block, see `alloc()` implementation for more details.
ASMJIT_ASSERT(curBlock == &Zone_zeroBlock || curBlock->getRemainingSize() < size);
cur = static_cast<Chunk*>(::malloc(sizeof(Chunk) - sizeof(void*) + chunkSize));
if (cur == NULL)
return NULL;
cur->prev = NULL;
cur->pos = 0;
cur->size = chunkSize;
if (_chunks != &Zone_zeroChunk)
cur->prev = _chunks;
_chunks = cur;
uint8_t* p = cur->data + cur->pos;
cur->pos += size;
ASMJIT_ASSERT(cur->pos <= cur->size);
return (void*)p;
// If the `Zone` has been cleared the current block doesn't have to be the
// last one. Check if there is a block that can be used instead of allocating
// a new one. If there is a `next` block it's completely unused, we don't have
// to check for remaining bytes.
Block* next = curBlock->next;
if (next != NULL && next->getBlockSize() >= size) {
next->pos = next->data + size;
_blocks = next;
return static_cast<void*>(next->data);
}
void* Zone::_allocZeroed(size_t size) {
void* p = _alloc(size);
// Prevent arithmetic overflow.
if (blockSize > ~static_cast<size_t>(0) - sizeof(Block))
return NULL;
Block* newBlock = static_cast<Block*>(::malloc(sizeof(Block) - sizeof(void*) + blockSize));
if (newBlock == NULL)
return NULL;
newBlock->pos = newBlock->data + size;
newBlock->end = newBlock->data + blockSize;
newBlock->prev = NULL;
newBlock->next = NULL;
if (curBlock != &Zone_zeroBlock) {
newBlock->prev = curBlock;
curBlock->next = newBlock;
// Does only happen if there is a next block, but the requested memory
// can't fit into it. In this case a new buffer is allocated and inserted
// between the current block and the next one.
if (next != NULL) {
newBlock->next = next;
next->prev = newBlock;
}
}
_blocks = newBlock;
return static_cast<void*>(newBlock->data);
}
void* Zone::allocZeroed(size_t size) {
void* p = alloc(size);
if (p != NULL)
::memset(p, 0, size);
return p;
}
@@ -132,7 +150,7 @@ char* Zone::sdup(const char* str) {
if (str == NULL)
return NULL;
size_t len = strlen(str);
size_t len = ::strlen(str);
if (len == 0)
return NULL;
@@ -153,17 +171,16 @@ char* Zone::sformat(const char* fmt, ...) {
if (fmt == NULL)
return NULL;
char buf[256];
char buf[512];
size_t len;
va_list ap;
va_start(ap, fmt);
len = vsnprintf(buf, 256, fmt, ap);
va_end(ap);
len = IntUtil::iMin<size_t>(len, 255);
len = vsnprintf(buf, ASMJIT_ARRAY_SIZE(buf) - 1, fmt, ap);
buf[len++] = 0;
va_end(ap);
return static_cast<char*>(dup(buf, len));
}

View File

@@ -23,28 +23,53 @@ namespace asmjit {
// [asmjit::Zone]
// ============================================================================
//! Fast incremental memory allocator.
//! Zone memory allocator.
//!
//! Memory allocator designed to allocate small objects that will be invalidated
//! (freed) all at once.
//! Zone is an incremental memory allocator that allocates memory by simply
//! incrementing a pointer. It allocates blocks of memory by using standard
//! C library `malloc/free`, but divides these blocks into smaller segments
//! requirested by calling `Zone::alloc()` and friends.
//!
//! Zone memory allocators are designed to allocate data of short lifetime. The
//! data used by `Assembler` and `Compiler` has a very short lifetime, thus, is
//! allocated by `Zone`. The advantage is that `Zone` can free all of the data
//! allocated at once by calling `clear()` or `reset()`.
struct Zone {
// --------------------------------------------------------------------------
// [Chunk]
// [Block]
// --------------------------------------------------------------------------
//! \internal
//!
//! One allocated chunk of memory.
struct Chunk {
//! Get count of remaining (unused) bytes in chunk.
ASMJIT_INLINE size_t getRemainingSize() const { return size - pos; }
//! A single block of memory.
struct Block {
// ------------------------------------------------------------------------
// [Accessors]
// ------------------------------------------------------------------------
//! Link to previous chunk.
Chunk* prev;
//! Position in this chunk.
size_t pos;
//! Size of this chunk (in bytes).
size_t size;
//! Get the size of the block.
ASMJIT_INLINE size_t getBlockSize() const {
return (size_t)(end - data);
}
//! Get count of remaining bytes in the block.
ASMJIT_INLINE size_t getRemainingSize() const {
return (size_t)(end - pos);
}
// ------------------------------------------------------------------------
// [Members]
// ------------------------------------------------------------------------
//! Current data pointer (pointer to the first available byte).
uint8_t* pos;
//! End data pointer (pointer to the first invalid byte).
uint8_t* end;
//! Link to the previous block.
Block* prev;
//! Link to the next block.
Block* next;
//! Data.
uint8_t data[sizeof(void*)];
@@ -56,38 +81,45 @@ struct Zone {
//! Create a new instance of `Zone` allocator.
//!
//! The `chunkSize` parameter describes the size of the chunk. If `alloc()`
//! requires more memory than `chunkSize` then a bigger chunk will be
//! allocated, however `chunkSize` will not be changed.
ASMJIT_API Zone(size_t chunkSize);
//! Destroy `Zone` instance.
//! The `blockSize` parameter describes the default size of the block. If the
//! `size` parameter passed to `alloc()` is greater than the default size
//! `Zone` will allocate and use a larger block, but it will not change the
//! default `blockSize`.
//!
//! Destructor released all chunks allocated by `Zone`. The `reset()` member
//! function does the same without actually destroying `Zone` object itself.
//! It's not required, but it's good practice to set `blockSize` to a
//! reasonable value that depends on the usage of `Zone`. Greater block sizes
//! are generally safer and performs better than unreasonably low values.
ASMJIT_API Zone(size_t blockSize);
//! Destroy the `Zone` instance.
//!
//! This will destroy the `Zone` instance and release all blocks of memory
//! allocated by it. It performs implicit `reset()`.
ASMJIT_API ~Zone();
// --------------------------------------------------------------------------
// [Clear / Reset]
// --------------------------------------------------------------------------
//! Reset the `Zone` releasing all chunks allocated.
//! Clear the `Zone`, but keep all blocks allocated so they can be reused.
//!
//! Calling `clear()` will release all chunks allocated by `Zone` except the
//! first one that will be reused if needed.
//! This is the preferred way of invalidating objects allocated by `Zone`.
ASMJIT_API void clear();
//! Reset the `Zone` releasing all chunks allocated.
//! Reset the `Zone` releasing all blocks allocated.
//!
//! Calling `reset()` will release all chunks allocated by `Zone`.
//! Calling `reset()` does complete cleanup, it releases all blocks allocated
//! by `Zone`.
ASMJIT_API void reset();
// --------------------------------------------------------------------------
// [Accessors]
// --------------------------------------------------------------------------
//! Get chunk size.
ASMJIT_INLINE size_t getChunkSize() const { return _chunkSize; }
//! Get the default block size.
ASMJIT_INLINE size_t getBlockSize() const {
return _blockSize;
}
// --------------------------------------------------------------------------
// [Alloc]
@@ -95,75 +127,70 @@ struct Zone {
//! Allocate `size` bytes of memory.
//!
//! Pointer allocated by this way will be valid until `Zone` object is
//! destroyed. To create class by this way use placement `new` and `delete`
//! operators:
//! Pointer returned is valid until the `Zone` instance is destroyed or reset
//! by calling `clear()` or `reset()`. If you plan to make an instance of C++
//! from the given pointer use placement `new` and `delete` operators:
//!
//! ~~~
//! // Example of simple class allocation.
//! using namespace asmjit
//! using namespace asmjit;
//!
//! // Your class.
//! class Object {
//! // members...
//! };
//! class SomeObject { ... };
//!
//! // Your function
//! void f() {
//! // Create zone object with chunk size of 65536 bytes.
//! // Create Zone with default block size of 65536 bytes.
//! Zone zone(65536);
//!
//! // Create your objects using zone object allocating, for example:
//! Object* obj = new(zone.alloc(sizeof(YourClass))) Object();
//! Object* obj = static_cast<Object*>( zone.alloc(sizeof(SomeClass)) );
//
//! if (obj == NULL) {
//! // Handle out of memory error.
//! }
//!
//! // To instantiate class placement `new` and `delete` operators can be used.
//! new(obj) Object();
//!
//! // ... lifetime of your objects ...
//!
//! // Destroy your objects:
//! // To destroy the instance (if required).
//! obj->~Object();
//!
//! // Zone destructor will free all memory allocated through it, you can
//! // call `zone.reset()` if you wan't to reuse current `Zone`.
//! }
//! // Reset of destroy `Zone`.
//! zone.reset();
//! ~~~
ASMJIT_INLINE void* alloc(size_t size) {
Chunk* cur = _chunks;
Block* cur = _blocks;
if (cur->getRemainingSize() < size)
uint8_t* ptr = cur->pos;
size_t remainingBytes = (size_t)(cur->end - ptr);
if (remainingBytes < size)
return _alloc(size);
uint8_t* p = cur->data + cur->pos;
cur->pos += size;
ASMJIT_ASSERT(cur->pos <= cur->size);
ASMJIT_ASSERT(cur->pos <= cur->end);
return (void*)p;
return (void*)ptr;
}
//! Like `alloc()`, but the return is casted to `T*`.
//! Allocate `size` bytes of zeroed memory.
//!
//! See \ref alloc() for more details.
ASMJIT_API void* allocZeroed(size_t size);
//! Like `alloc()`, but the return pointer is casted to `T*`.
template<typename T>
ASMJIT_INLINE T* allocT(size_t size = sizeof(T)) {
return static_cast<T*>(alloc(size));
}
//! \internal
ASMJIT_API void* _alloc(size_t size);
//! Allocate `size` bytes of zeroed memory.
ASMJIT_INLINE void* allocZeroed(size_t size) {
Chunk* cur = _chunks;
if (cur->getRemainingSize() < size)
return _allocZeroed(size);
uint8_t* p = cur->data + cur->pos;
cur->pos += size;
ASMJIT_ASSERT(cur->pos <= cur->size);
::memset(p, 0, size);
return (void*)p;
//! Like `allocZeroed()`, but the return pointer is casted to `T*`.
template<typename T>
ASMJIT_INLINE T* allocZeroedT(size_t size = sizeof(T)) {
return static_cast<T*>(allocZeroed(size));
}
//! \internal
ASMJIT_API void* _allocZeroed(size_t size);
ASMJIT_API void* _alloc(size_t size);
//! Helper to duplicate data.
ASMJIT_API void* dup(const void* data, size_t size);
@@ -178,10 +205,15 @@ struct Zone {
// [Members]
// --------------------------------------------------------------------------
//! Last allocated chunk of memory.
Chunk* _chunks;
//! Default chunk size.
size_t _chunkSize;
//! The current block.
Block* _blocks;
//! Default block size.
size_t _blockSize;
};
enum {
//! Zone allocator overhead.
kZoneOverhead = static_cast<int>(sizeof(Zone::Block) - sizeof(void*)) + kMemAllocOverhead
};
//! \}