Browse Source

Merge incomplete branch 'tree_alloc'

master
Cameron Weinfurt 1 year ago
parent
commit
a0155d66dd
  1. 7
      .gitignore
  2. 11
      Makefile
  3. 13
      alloc_api.h
  4. 41
      allocator_internal.h
  5. 132
      main.c
  6. 122
      paper.tex
  7. 673
      tree_alloc.c
  8. 33
      util.c

7
.gitignore

@ -1 +1,8 @@
*.o
the_alloc
vgcore*
debug_log.txt
paper.log
paper.aux
paper.pdf

11
Makefile

@ -1,12 +1,13 @@
objs = main.o tree_alloc.o
objs = main.o tree_alloc.o util.o
CC = clang
COMMON_FLAGS = -std=c11
COMMON_FLAGS = -std=gnu11
OUT_NAME = the_alloc
dev: CFLAGS= $(COMMON_FLAGS) -g -D UNIT_TESTS
dev: CFLAGS= $(COMMON_FLAGS) -g -D UNIT_TESTS -D DEBUG=1
dev: $(OUT_NAME)
valgrind ./$(OUT_NAME)
rm -f vgcore.*
valgrind ./$(OUT_NAME)
release: CFLAGS= $(COMMON_FLAGS) -O2
release: clean $(OUT_NAME)
@ -18,7 +19,7 @@ $(OUT_NAME): $(objs)
clean:
rm -f *.o $(OUT_NAME)
main.o: main.c
main.o: main.c alloc_api.h
$(CC) $(CFLAGS) -c main.c -o main.o
tree_alloc.o: tree_alloc.c allocator_internal.h

13
alloc_api.h

@ -0,0 +1,13 @@
#include <stdint.h>
struct Arena {
void *root_freespace;
void *root_treealloc;
void *(*get_new_region)(uintptr_t);
void (*error)(char*);
};
void unalloc(struct Arena *arena, void *addr);
void *alloc(struct Arena *arena, uintptr_t size, uintptr_t align);

41
allocator_internal.h

@ -8,23 +8,40 @@ const char RT_FREESPACE = 0;
const char RT_TREE_NODE = 1;
const char RT_WATERMARK = 2;
struct TopLevel {
const char COLOR_RED = 0;
const char COLOR_BLACK = 1;
const uintptr_t MIN_NEW_MEM_SIZE = 4096;
typedef struct Arena {
struct FreeSpace *root_freespace;
struct TreeAlloc *root_treealloc;
};
void *(*get_new_region)(uintptr_t);
void (*error)(char*);
} Arena;
// All three of these types should be memory-compatible with each other
// Before I forget to write it down: It is assumed that the beginning of every allocation region (or
// free space region) is aligned to the alignment of all three of these types (or WatermarkAlloc since
// it has at least all the fields of the other one). This allows for the allocation record to appear
// at the beginning of the region and still be aligned. As a consequence, all sizes are a multiple of
// the alignment.
typedef struct TreeAlloc {
char type; // Should be RT_TREE_NODE
char color;
struct TreeAlloc *parent;
struct TreeAlloc *left;
struct TreeAlloc *right;
uintptr_t size;
struct TreeAlloc *before;
struct TreeAlloc *after;
} TreeAlloc;
typedef struct FreeSpace {
char type; // Should be RT_FREESPACE
char color;
struct FreeSpace *parent;
struct FreeSpace *left;
struct FreeSpace *right;
@ -33,17 +50,31 @@ typedef struct FreeSpace {
typedef struct WatermarkAlloc {
char type; // Should be RT_WATERMARK
char color;
struct TreeAlloc *parent;
struct TreeAlloc *left;
struct TreeAlloc *right;
uintptr_t size;
struct TreeAlloc *before;
struct TreeAlloc *after;
int num_allocs;
void *next_alloc;
} WatermarkAlloc;
void* align_after(void* address, int align);
TreeAlloc *insert_node_at(void *address, int padding, int align, int size);
void* align_after(void* address, uintptr_t align);
uintptr_t lcm(uintptr_t a, uintptr_t b);
uintptr_t gcd(uintptr_t a, uintptr_t b);
TreeAlloc *search_by_address(TreeAlloc *root, void *address);
TreeAlloc *search_by_size(TreeAlloc *root, int padding, int align, int size);
TreeAlloc *search_by_size(TreeAlloc *root, uintptr_t padding, uintptr_t align, uintptr_t size);
TreeAlloc *get_sibling(TreeAlloc *p, TreeAlloc *ta);
void rotate_left(TreeAlloc **root_ptr, TreeAlloc *ta);
void rotate_right(TreeAlloc **root_ptr, TreeAlloc *ta);
void repair_tree_after_insert(TreeAlloc **root_ptr, TreeAlloc *ta);
void remove_node(TreeAlloc** root_ptr, TreeAlloc* node);
void insert_singleton(TreeAlloc **root_ptr, TreeAlloc *to_insert);
void insert_by_size(TreeAlloc** root_ptr, TreeAlloc* to_insert);
void insert_by_addr(TreeAlloc** root_ptr, TreeAlloc* to_insert);
void unalloc(Arena *arena, void *addr);
void *alloc(Arena *arena, uintptr_t size, uintptr_t align);
#endif

132
main.c

@ -1,6 +1,136 @@
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
#include <stddef.h>
#include <unistd.h>
#include <sys/mman.h>
#include <string.h>
#include "alloc_api.h"
void *main_get_new_region(uintptr_t size) {
void *m = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (m == MAP_FAILED) {
return NULL;
}
return m;
}
void error(char *msg) {
printf("=== ALLOCATOR ERROR ===\n%s\n====== END ERROR ======\n", msg);
}
struct AllocationRecord {
uintptr_t size;
void *allocation;
void *reference;
int id;
struct AllocationRecord *next;
struct AllocationRecord *prev;
};
void insert_record(struct AllocationRecord *dummy, void *allocation, uintptr_t size) {
struct AllocationRecord *record = malloc(sizeof(struct AllocationRecord));
record->size = size;
record->allocation = allocation;
record->reference = malloc(size);
record->next = dummy;
record->prev = dummy->prev;
record->id = dummy->id;
dummy->prev->next = record;
dummy->prev = record;
dummy->size += 1;
dummy->id += 1;
memcpy(record->reference, allocation, size);
}
void delete_record(struct AllocationRecord *dummy, struct AllocationRecord *delete) {
free(delete->reference);
delete->prev->next = delete->next;
delete->next->prev = delete->prev;
free(delete);
dummy->size -= 1;
}
struct AllocationRecord *get_record(struct AllocationRecord *dummy, int which) {
struct AllocationRecord *head = dummy->next;
while (which--) {
head = head->next;
}
return head;
}
int validate_record(struct AllocationRecord *record) {
if (memcmp(record->allocation, record->reference, record->size) != 0) {
return 0;
}
return 1;
}
void new_region(struct AllocationRecord *dummy, struct Arena *arena) {
uintptr_t size = 1 + (rand() % 8192);
uintptr_t align = 1 << (rand() % 4);
printf("alloc'ing a region of size %lu with alignment %lu, id %i\n", size, align, dummy->id);
void *region = alloc(arena, size, align);
if (region == NULL) {
printf("memory allocator broke, we have 1L though\n");
return;
}
for (int ii = 0; ii < size; ii++) {
((char*) region)[ii] = (char) rand();
}
insert_record(dummy, region, size);
}
void delete_region(struct AllocationRecord *dummy, struct Arena *arena) {
if (dummy->size == 0) {
return;
}
uintptr_t which = rand() % dummy->size;
struct AllocationRecord *zap = get_record(dummy, which);
printf("dealloc'ing a region of size %lu, id %i\n", zap->size, zap->id);
unalloc(arena, zap->allocation);
delete_record(dummy, zap);
}
int act(struct AllocationRecord *dummy, struct Arena *arena) {
if (rand() & 1) {
new_region(dummy, arena);
} else {
delete_region(dummy, arena);
}
int die = 0;
struct AllocationRecord *head = dummy->next;
while (head != dummy) {
if (!validate_record(head)) {
//printf("validation failed at id %i\n", head->id);
die = 1;
}
head = head->next;
}
return die;
}
int main() {
printf("Hello, World!\n");
srand(1);
struct Arena arena = {
NULL,
NULL,
main_get_new_region,
error,
};
void *reg = alloc(&arena, 20, 4);
unalloc(&arena, reg);
struct AllocationRecord dummy = {
.size = 0,
.allocation = NULL,
.reference = NULL,
.id = 0,
};
dummy.next = &dummy;
dummy.prev = &dummy;
for (int ii = 0; ii < 100; ii++) {
act(&dummy, &arena);
}
return 0;
}

122
paper.tex

@ -0,0 +1,122 @@
\documentclass{article}
\begin{document}
\title{A Memory Allocator}
\author{Cameron Weinfurt, Thomas Johnson}
\maketitle
\section{Introduction}
When implementing the functionality of abstract data structures, it becomes necessary to have an array
whose size is not known at compile time. This is problematic in languages like C and C++ where the
size of an array must be known when compiling in order to calculate how bih the stack frame must be.
The solution is to forgo having the array existing on the stack and not even dedicate space at compile
time for the array. Instead, the array is created during runtime once the size of the array can be
determined. This is known as dynamic memory allocation. A special region in a process's memory is
dedicated for these kinds of allocations, called the heap, while a memory allocator keeps track of
this space's usage. How the allocator handles memory that is no longer in use and situations in which
a region must be resized depends on how it is implemented. In addition, the memory allocator has to
balance how much memory that it uses for record keeping while also minimizing the amount of CPU cycles
required to manage the memory region it has been given.
In ANSI C, the standard library provides a memory allocator for general purpose use in programs.
Dynamic memory allocation is performed through the {\tt malloc()} library call in which the caller
passes the desired size of the allocation and it returns a pointer to the allocated memory region. It
must be noted that malloc does not initalize the returned memory region with any value, so the caller
must initalize the array itself. However, the standard library also provides the {\tt calloc()}
library call in which the allocated memory is initalized with zero in every byte. Resizing of an
allocation is performed using the {\tt realloc()} and {\tt reallocarray()} library calls, which will
either grow the allocation in place or move the allocation to a space in which the new size can fit.
The program must signify to the allocator that an memory region is to be marked free through the {\tt
free()} libary call. Should it fail to notify the allocator that an allocation is no longer in use
before all of its references go out of scope, it will become impossible to access the underlying
data or use the space it took up; a
memory leak.
Other languages' standard libraries can provide memory allocators or additional data structures for
dynamic allocation using other techniques. Rather returning a pointer as a reference to the
allocation, languages like C++ and Rust provide smart references that determines when the underlying
memory allocation can be freed by detecting when all of its references have gone out of scope. Runtime
languages like those running on the Java Virtual Machine or Microsoft's Common Language Interface also
provide smart pointers, but wait to free the unused memory regions until a scheduled batch process
occurs known as a garbage collection. Higher order languages like Python and Lua completely abstract
away that dynamic allocation is occuring by only allowing abstract data structures to be created by
the programmer, allowing for their implementation to handle the underlying allocations needed
transparently.
In practice, abstract data structures do not allocate in the same way. It is possible to categorize
them into two groups based on how they allocate. For data structures like vectors and smart strings, a
single allocation is made at its creation and then resized as data is either added or removed. This is
the first group. The second group consists of data structures like linked lists and trees, where many
allocations and frees are requested, but each allocation is fixed in size. Rather than handling
dynamic allocation request using one method, abstract data structures could instead give hints to the
allocator as to what kind of allocations it should be making. Such an allocator could then be
constructed to take advantage of these hints and optimize accordingly. This is what this paper aims to
demonstrate.
\section{Implementation}
\subsection{The Tree Allocator}
Internally, the allocator is made of two self-balancing binary search trees in which one keeps records
of free space available to the allocator while the other keeps records of allocated memory. Both trees
are sorted using the size of their respective regions, though they can be search based on location in
memory as well. Each node can be one of three types depending on their purpose. The type is used to
determine which struct to represent the node with; the similar footprint permitting pointer
polymorphism. A red-black tree was chosen to perform the self-balancing due to the minimal cost of
adding a color field to each node.
To perform an allocation, the allocator first searches for within the free space tree for a memory
block of suitable size. If it cannot find one, it requests the operating system for additional memory
to mapped into the process before pushing the new space onto the tree and searching again. Once a node
representing a memory region of sufficent size is found, it is removed from the free-space tree. The
underlying space is then split to fit the new allocation and leave the excess space unallocated. The
allocated space as a new node is pushed onto the allocations tree while the excess space also in new
nodes are pushed back ono the free-space tree. In particular, the allocator attempts to place the new
allocation in the center of the free space in order to minimize the chances of resizing causing a
move. Deallocations are handled in a similar manner. When an address is requested to be freed, the
allocator searches for the corresponding node in the allocations tree. This node is then popped off
the allocations tree and pushed onto the free-space tree. In addition, if it is found that this node
is surrounded by unallocated memory after being pushed onto the free-space tree, it will merge the
nodes together. This keeps fragmentation at a minimum and speeds up subsequent allocations and
deallocation.
\subsection{The Watermark Allocator}
On its own, the watermark allocator present many problems that make is unfeasible to use as an
allocator. This model of allocator is simply a stack that cannot have elements popped off of it while
also holding a reference counter. The obvious problem with this is that frees ultimately are leaks
under a watermark allocator. Unrestricted, a watermark allocator will eventually run out of memory
even though free space may exist behind its stack pointer. This simplistic model does not come without
its benefits; however, being that an allocation requires very little overhead and metadata to handle.
The solution that was derived to take advantage of this property was to use the tree allocator to
manage a series of finite sized watermark allocators. The implementation does not create an instance
of a watermark allocator until a request for a fixed sized allocation is made. It is limited to a
space of 4096 bytes, enforcing that the allocator be used for small, fixed size allocations. Larger
allocations will either fail or be allocated using the tree allocator instead. Should an allocator run
out of space, a new one is created and the allocation is performed on that new allocator. In addition,
it is stored as a node within the tree allocator, meaning the last reference to the memory region
will be the global allocator itself, which will free the space through the tree allocator
automatically when the reference count on the space goes to zero.
\section{Results}
% TODO: Add figures the LaTeX way
To evaluate the effectiveness of the allocator, glibc's malloc was used as a benchmark.
Tree alloc vs. Watermark alloc vs. Glibc Malloc with repeated allocs of size 20:
Tree alloc vs. Glibc Malloc with repeated allocs of size 8000:
%## Tree alloc vs. Watermark alloc vs Glibc Malloc with repeated frees of size 20:
%## Tree alloc vs. Glibc Malloc with repeated frees of size 8000:
%## Tree alloc vs. Glibc Realloc with repeatedly resizing allocations.
A set of 3 allocations were made of size 20. They were doubled in size repeatedly in sequence of their allocation.
\section{Conclusion}
\end{document}

673
tree_alloc.c

@ -1,44 +1,669 @@
#include <stddef.h>
#include <stdalign.h>
#include "allocator_internal.h"
TreeAlloc *insert_node_at(void *address, int padding, int align, int size) {
#define IS_BLACK_NODE(n) (n == NULL || n->color == COLOR_BLACK)
#define IS_RED_NODE(n) (n != NULL && n->color == COLOR_RED)
#ifdef DEBUG
#include <stdio.h>
int debug_tree_black_height(TreeAlloc *node) {
if (node == NULL) {
return 1;
}
return (IS_BLACK_NODE(node) ? 1 : 0) + debug_tree_black_height(node->left);
}
void debug_print_node(int indent, TreeAlloc *node, int bad) {
for (int ii = 0; ii < indent; ii++)
printf(" ");
if (IS_RED_NODE(node))
printf("\e[31m");
else if (bad)
printf("\e[30m]");
if (bad)
printf("\e[43m");
if (node)
printf("%p %lu\n", node, node->size);
else
printf("(nil) 0\n");
printf("\e[37m");
if (bad)
printf("\e[40m");
}
void debug_print_tree(int indent, void *p, int safe) {
TreeAlloc *node = (TreeAlloc*) p;
if (node != NULL) {
int bad = debug_tree_black_height(node->left) != debug_tree_black_height(node->right);
bad |= IS_RED_NODE(node) && (
IS_RED_NODE(node->left) ||
IS_RED_NODE(node->right) ||
IS_RED_NODE(node->parent) );
bad &= !safe;
debug_print_tree(indent + 1, node->left, safe);
debug_print_node(indent, node, bad);
debug_print_tree(indent + 1, node->right, safe);
}
}
#endif
TreeAlloc *insert_node_at(void *address, uintptr_t padding, uintptr_t align, uintptr_t size) {
return NULL;
}
/*
* Search for the node whose allocated region contains an address.
*/
TreeAlloc *search_by_address(TreeAlloc *root, void *address) {
TreeAlloc *head = root;
while (1) {
if (root < address) {
if (root->left)
root = root->left;
else
return root;
} else if (root > address) {
if (root->right)
root = root->right;
else
return root;
void *region_start = head;
void *region_end = head + head->size;
if (address < region_start) {
// The requested address is before this region's start.
if (head->left) {
// There is another region that comes before this one
// in memory.
head = head->left;
} else {
// This address is before any of the allocated regions.
return NULL;
}
} else if (address <= region_end) {
// The requested address is within the range of this region.
return head;
} else {
return root;
// The requested address is after this region's end.
if (head->right) {
// There is another region that comes after this one
// in memory.
head = head->right;
} else {
// This address is after any of the allocated regions.
return NULL;
}
}
}
}
TreeAlloc *search_by_size(TreeAlloc *root, int padding, int align, int size) {
static uintptr_t effective_size(TreeAlloc *head, uintptr_t padding, uintptr_t align) {
return head->size - (align_after(head + padding, align) - (void*) head);
}
/*
* This is the most optimistic estimate of size that we can use which also preserves the ordering over
* the tree. I had planned to use effective_size before I realized that it would break the tree
* ordering.
*/
static uintptr_t pessimistic_size(TreeAlloc *head, uintptr_t padding, uintptr_t align) {
return head->size - padding - align + 1;
}
TreeAlloc *search_by_size(TreeAlloc *root, uintptr_t padding, uintptr_t align, uintptr_t size) {
TreeAlloc *head = root;
while (1) {
if (root->size < size) {
if (root->left)
root = root->left;
else
return root;
} else if (root->size > address) {
if (root->right)
root = root->right;
else
return root;
uintptr_t esize = pessimistic_size(head, padding, align);
if (esize <= size) {
if (head->right == NULL) {
return NULL;
} else {
head = head->right;
}
} else {
if (head->left == NULL || pessimistic_size(head->left, padding, align) < size) {
return head;
} else {
head = head->left;
}
}
}
}
TreeAlloc *succ(TreeAlloc *el) {
if (el->right != NULL) {
el = el->right;
while (el->left != NULL) {
el = el->left;
}
return el;
}
while (el->parent != NULL && el == el->parent->right) {
el = el->parent;
}
return el->parent;
}
TreeAlloc *pred(TreeAlloc *el) {
if (el->left != NULL) {
el = el->left;
while (el->right != NULL) {
el = el->right;
}
return el;
}
while (el->parent != NULL && el == el->parent->left) {
el = el->parent;
}
return el->parent;
}
TreeAlloc *get_sibling(TreeAlloc *p, TreeAlloc *ta) {
if (!p)
return NULL;
else if (p->left == ta)
return p->right;
else
return p->left;
}
void rotate_left(TreeAlloc **root_ptr, TreeAlloc *ta) {
TreeAlloc *parent, *tmp;
tmp = ta->right;
parent = ta->parent;
if (tmp) {
ta->right = tmp->left;
tmp->left = ta;
tmp->parent = parent;
}
if (ta->right) ta->right->parent = ta;
ta->parent = tmp;
if (!parent) {
*root_ptr = tmp;
} else if (ta == parent->left) {
parent->left = tmp;
} else {
parent->right = tmp;
}
}
void rotate_right(TreeAlloc **root_ptr, TreeAlloc *ta) {
TreeAlloc *parent, *tmp;
tmp = ta->left;
parent = ta->parent;
if (tmp) {
ta->left = tmp->right;
tmp->right = ta;
tmp->parent = parent;
}
if (ta->left) ta->left->parent = ta;
ta->parent = tmp;
if (!parent) {
*root_ptr = tmp;
} else if (ta == parent->left) {
parent->left = tmp;
} else {
parent->right = tmp;
}
}
void repair_tree_after_insert(TreeAlloc **root_ptr, TreeAlloc *ta) {
TreeAlloc *parent = ta->parent;
if (ta == *root_ptr) {
ta->color = COLOR_BLACK;
} else if (IS_BLACK_NODE(parent)) {
return;
} else {
TreeAlloc *uncle = get_sibling(parent->parent, parent);
TreeAlloc *grandparent = parent->parent;
if (IS_RED_NODE(uncle)) {
parent->color = COLOR_BLACK;
uncle->color = COLOR_BLACK;
grandparent->color = COLOR_RED;
repair_tree_after_insert(root_ptr, grandparent);
} else {
return root;
if (parent->left == ta) {
if (grandparent->left == parent) {
rotate_right(root_ptr, grandparent);
grandparent->color = COLOR_RED;
parent->color = COLOR_BLACK;
} else {
rotate_right(root_ptr, parent);
rotate_left(root_ptr, grandparent);
grandparent->color = COLOR_RED;
ta->color = COLOR_BLACK;
}
} else {
if (grandparent->left == parent) {
rotate_left(root_ptr, parent);
rotate_right(root_ptr, grandparent);
grandparent->color = COLOR_RED;
ta->color = COLOR_BLACK;
} else {
rotate_left(root_ptr, grandparent);
grandparent->color = COLOR_RED;
parent->color = COLOR_BLACK;
}
}
}
}
}
// Inserts a node into an empty tree.
void insert_singleton(TreeAlloc **root_ptr, TreeAlloc *to_insert) {
#ifdef DEBUG
printf("= PRE-INSERT-SINGLETON =\n");
printf("===== CURRENT TREE =====\n");
debug_print_tree(0, *root_ptr, 0);
printf("===== END OF TREES =====\n");
#endif
*root_ptr = to_insert;
to_insert->parent = NULL;
to_insert->color = COLOR_BLACK;
#ifdef DEBUG
printf("= POST-INSERT-SINGLETON =\n");
printf("===== CURRENT TREE =====\n");
debug_print_tree(0, *root_ptr, 0);
printf("===== END OF TREES =====\n");
#endif
}
void insert_by_size(TreeAlloc** root_ptr, TreeAlloc* to_insert) {
#ifdef DEBUG
printf("=== PRE-INSERT-BY-SIZE ===\n");
printf("===== INSERTING =====\n");
debug_print_node(0, to_insert, 0);
printf("===== CURRENT TREE =====\n");
debug_print_tree(0, *root_ptr, 0);
printf("===== END OF TREES =====\n");
#endif
TreeAlloc *tree_ptr = *root_ptr;
while (1) {
if (to_insert->size < tree_ptr->size) {
if (tree_ptr->left) {
tree_ptr = tree_ptr->left;
} else {
tree_ptr->left = to_insert;
to_insert->parent = tree_ptr;
break;
}
} else {
if (tree_ptr->right) {
tree_ptr = tree_ptr->right;
} else {
tree_ptr->right = to_insert;
to_insert->parent = tree_ptr;
break;
}
}
}
to_insert->color = COLOR_RED;
repair_tree_after_insert(root_ptr, to_insert);
#ifdef DEBUG
printf("== POST-INSERT-FIXUP ===\n");
printf("===== CURRENT TREE =====\n");
debug_print_tree(0, *root_ptr, 0);
printf("===== END OF TREES =====\n");
#endif
}
void insert_by_addr(TreeAlloc** root_ptr, TreeAlloc* to_insert) {
#ifdef DEBUG
printf("=== PRE-INSERT-BY-ADDR ===\n");
printf("===== INSERTING =====\n");
debug_print_tree(0, to_insert, 0);
printf("===== CURRENT TREE =====\n");
debug_print_tree(0, *root_ptr, 0);
printf("===== END OF TREES =====\n");
#endif
TreeAlloc *tree_ptr = *root_ptr;
while (1) {
if (to_insert < tree_ptr) {
if (tree_ptr->left) {
tree_ptr = tree_ptr->left;
} else {
tree_ptr->left = to_insert;
to_insert->parent = tree_ptr;
break;
}
} else {
if (tree_ptr->right) {
tree_ptr = tree_ptr->right;
} else {
tree_ptr->right = to_insert;
to_insert->parent = tree_ptr;
break;
}
}
}
to_insert->color = COLOR_RED;
repair_tree_after_insert(root_ptr, to_insert);
#ifdef DEBUG
printf("== POST-INSERT-FIXUP ===\n");
printf("===== CURRENT TREE =====\n");
debug_print_tree(0, *root_ptr, 0);
printf("===== END OF TREES =====\n");
#endif
}
void replace_node(TreeAlloc **root_ptr, TreeAlloc *node, TreeAlloc *replace) {
if (!node->parent) {
*root_ptr = replace;
} else {
if (node == node->parent->left)
node->parent->left = replace;
else
node->parent->right = replace;
}
if (replace) replace->parent = node->parent;
}
void repair_after_remove(TreeAlloc **root_ptr, TreeAlloc *parent, TreeAlloc *node) {
#ifdef DEBUG
printf("delete fixup at %p -> %p\n", parent, node);
#endif
// In theory, the last two conditions should be the same ...
if (IS_RED_NODE(node) || (node != NULL && node == *root_ptr)) {
node->color = COLOR_BLACK;
} else {
TreeAlloc *sibling = get_sibling(parent, node);
if (IS_RED_NODE(sibling)) {
if (parent->left == node) {
rotate_left(root_ptr, parent);
} else {
rotate_right(root_ptr, parent);
}
// The rotate shouldn't touch the parent relationship of `node`
parent->parent->color = COLOR_BLACK;
parent->color = COLOR_RED;
sibling = get_sibling(parent, node);
}
if (IS_BLACK_NODE(sibling->left) && IS_BLACK_NODE(sibling->right)) {
if (node != NULL)
node->color = COLOR_BLACK;
sibling->color = COLOR_RED;
repair_after_remove(root_ptr, parent->parent, parent);
} else {
if (parent->left == node && IS_BLACK_NODE(sibling->right)) {
rotate_right(root_ptr, sibling);
sibling->color = COLOR_RED;
sibling = parent->right;
sibling->color = COLOR_BLACK;
}
if (parent->right == node && IS_BLACK_NODE(sibling->left)) {
rotate_left(root_ptr, sibling);
sibling->color = COLOR_RED;
sibling = parent->left;
sibling->color = COLOR_BLACK;
}
if (parent->left == node) {
rotate_left(root_ptr, parent);
} else {
rotate_right(root_ptr, parent);
}
if (node != NULL)
node->color = COLOR_BLACK;
TreeAlloc *uncle = get_sibling(parent->parent, parent);
if (uncle != NULL)
uncle->color = COLOR_BLACK;
char swap = parent->color;
parent->color = parent->parent->color;
parent->parent->color = swap;
}
}
}
void remove_node(TreeAlloc **root_ptr, TreeAlloc *to_remove) {
char do_repair = 0;
char old_color;
#ifdef DEBUG
printf("====== PRE-REMOVE ======\n");
printf("======= REMOVING =======\n");
debug_print_node(0, to_remove, 0);
printf("===== CURRENT TREE =====\n");
debug_print_tree(0, *root_ptr, 0);
printf("===== END OF TREES =====\n");
#endif
TreeAlloc *replace, *parent_of_replace;
TreeAlloc *parent = to_remove->parent;
if (!to_remove->left) {
#ifdef DEBUG
printf("code path 1l\n");
#endif
replace = to_remove->right;
parent_of_replace = to_remove->parent;
do_repair = to_remove->color == COLOR_BLACK;
replace_node(root_ptr, to_remove, replace);
} else if (!to_remove->right) {
#ifdef DEBUG
printf("code path 1r\n");
#endif
replace = to_remove->left;
parent_of_replace = to_remove->parent;
do_repair = to_remove->color == COLOR_BLACK;
replace_node(root_ptr, to_remove, replace);
} else {
#ifdef DEBUG
printf("code path 2\n");
#endif
TreeAlloc *tmp = succ(to_remove);
replace = tmp->right;
do_repair = tmp->color == COLOR_BLACK;
if (tmp != to_remove->right) {
replace_node(root_ptr, tmp, replace);
tmp->right = to_remove->right;
to_remove->right->parent = tmp;
parent_of_replace = tmp->parent;
} else {
parent_of_replace = tmp;
}
replace_node(root_ptr, to_remove, tmp);
tmp->color = to_remove->color;
tmp->left = to_remove->left;
to_remove->left->parent = tmp;
}
// Make sure that it doesn't have any tree pointers it shouldn't have.
to_remove->parent = to_remove->left = to_remove->right = NULL;
#ifdef DEBUG
printf("==== PRE-REMOVE-FIXUP ===\n");
printf("===== CURRENT TREE =====\n");
debug_print_tree(0, *root_ptr, 1);
printf("===== END OF TREES =====\n");
printf("considering fixing up %p -> %p\n", parent_of_replace, replace);
#endif
if (replace && parent_of_replace == NULL) {
replace->color = COLOR_BLACK;
} else if (parent_of_replace != NULL && do_repair) {
repair_after_remove(root_ptr, parent_of_replace, replace);
}
#ifdef DEBUG
printf("=== POST-REMOVE ===\n");
printf("===== CURRENT TREE =====\n");
debug_print_tree(0, *root_ptr, 0);
printf("===== END OF TREES =====\n");
#endif
}
TreeAlloc *get_new_region(Arena *arena, uintptr_t size, uintptr_t padding, uintptr_t align) {
uintptr_t realsize = size + align + alignof(WatermarkAlloc) + padding - 1;
#ifdef DEBUG
printf("Attemping request of size %ld\n", realsize);
#endif
if (realsize < MIN_NEW_MEM_SIZE) {
realsize = MIN_NEW_MEM_SIZE;
}
TreeAlloc *reg = (TreeAlloc *) arena->get_new_region(realsize);
if (reg == NULL) {
arena->error("can't allocate a new memory region!");
} else {
reg->parent = NULL;
reg->left = NULL;
reg->right = NULL;
reg->before = NULL;
reg->after = NULL;
reg->size = realsize;
}
return reg;
}
void unalloc(Arena *arena, void *addr) {
#ifdef DEBUG
printf("==== UNALLOCATING ====\n");
printf("=== FREESPACE TREE ===\n");
debug_print_tree(0, arena->root_freespace, 0);
printf("=== TREEALLOC TREE ===\n");
debug_print_tree(0, arena->root_treealloc, 0);
printf("==== END OF TREES ====\n");
#endif
if (arena->root_treealloc == NULL) {
arena->error("attempt to unallocate when there are no allocations!");
return;
}
// Find the node this address belongs to
TreeAlloc *node = search_by_address(arena->root_treealloc, addr);
if (node == NULL) {
arena->error("attempt to free memory outside any allocations!");
return;
}
// Handle the watermark allocator in this region
if (node->type == RT_WATERMARK) {
// TODO: handle watermark deallocation
return;
}
// Get rid of it
remove_node(&arena->root_treealloc, node);
// If there's free space on either side of it, merge it with the free space into a bigger chunk of
// free space.
uintptr_t size = node->size;
FreeSpace *start = (FreeSpace*) node;
if (node->before != NULL && node->before->type == RT_FREESPACE) {
start = (FreeSpace*) node->before;
size += node->before->size;
remove_node((TreeAlloc**) &arena->root_freespace, node->before);
}
if (node->after != NULL && node->after->type == RT_FREESPACE) {
size += node->after->size;
remove_node((TreeAlloc**) &arena->root_freespace, node->after);
}
start->type = RT_FREESPACE;
start->size = size;
// And finally, insert the resulting free space.
if (arena->root_freespace == NULL) {
insert_singleton((TreeAlloc**) &arena->root_freespace, (TreeAlloc*) start);
} else {
TreeAlloc *insert_point = search_by_size((TreeAlloc*) arena->root_freespace, 0, 1, size);
if (insert_point == NULL) {
insert_by_size((TreeAlloc**) &arena->root_freespace, (TreeAlloc*) start);
} else {
insert_by_size((TreeAlloc**) &arena->root_freespace, (TreeAlloc*) start);
}
}
}
void *alloc(Arena *arena, uintptr_t size, uintptr_t align) {
uintptr_t actual_align = lcm(alignof(struct WatermarkAlloc), align);
#ifdef DEBUG
printf("==== ALLOCATING =====\n");
printf("=== FREESPACE TREE ===\n");
debug_print_tree(0, arena->root_freespace, 0);
printf("=== TREEALLOC TREE ===\n");
debug_print_tree(0, arena->root_treealloc, 0);
printf("==== END OF TREES ====\n");
#endif
TreeAlloc *region;
if (arena->root_freespace == NULL) {
// Handle being out of freespace.
#ifdef DEBUG
printf("Out of freespace nodes; getting more\n");
#endif
region = get_new_region(arena, size, sizeof(TreeAlloc), actual_align);
} else {
region = search_by_size((TreeAlloc*) arena->root_freespace, sizeof(TreeAlloc), actual_align, size);
if (region == NULL) {
// Handle insufficient freespace or fragmentation.
#ifdef DEBUG
printf("Out of sufficiently large freespace nodes; getting more\n");
#endif
region = get_new_region(arena, size, sizeof(TreeAlloc), actual_align);
} else {
remove_node((TreeAlloc**) &arena->root_freespace, region);
}
}
void *true_end = align_after(align_after(((void*) region) + sizeof(TreeAlloc), actual_align) + size, alignof(WatermarkAlloc));
// The size of the new allocation (adjusted for region header and alignment
uintptr_t new_size = true_end - (void*) region;
// The size of the free space region following the new allocation
uintptr_t new_free_size = region->size - new_size;
region->right = NULL;
region->left = NULL;
region->type = RT_TREE_NODE;
region->size = size;
#ifdef DEBUG
printf("start: %p, end: %p, adjusted end: %p\n", region, ((void*) region) + size, true_end);
printf("size: %lu -> %lu\n", size, new_size);
printf("new_free_size: %lu\n", new_free_size);
#endif
if (arena->root_treealloc == NULL) {
insert_singleton((TreeAlloc**) &arena->root_treealloc, region);
} else {
insert_by_addr(&arena->root_treealloc, region);
}
if (region->size >= new_size + sizeof(FreeSpace)) {
// If there's enough free space after the allocation, use it!
region->size = new_size; // Safe because the allocated region tree is not sorted by size.
FreeSpace *new_free = (FreeSpace*) ((void*) region + new_size);
new_free->left = NULL;
new_free->right = NULL;
new_free->parent = NULL;
new_free->type = RT_FREESPACE;
new_free->size = new_free_size;
if (arena->root_freespace == NULL) {
insert_singleton((TreeAlloc**) &arena->root_freespace, (TreeAlloc*) new_free);
} else {
insert_by_size((TreeAlloc**) &arena->root_freespace, (TreeAlloc*) new_free);
}
// Set the region following this one to be the new free space
region->after = (TreeAlloc*) new_free;
} else {
// There isn't a free space after this one, so put the `next` pointer at the next allocated
// region if there is one.
region->after = search_by_address((TreeAlloc *) &arena->root_treealloc, region + region->size + 1);
}
// Are there any allocations before this one?
region->before = search_by_address((TreeAlloc *) &arena->root_treealloc, region - 1);
#ifdef DEBUG
printf("region is still at %p\n", region);
printf("=== POST-ALLOCATION ===\n");
printf("=== FREESPACE TREE ===\n");
debug_print_tree(0, arena->root_freespace, 0);
printf("=== TREEALLOC TREE ===\n");
debug_print_tree(0, arena->root_treealloc, 0);
printf("==== END OF TREES ====\n");
#endif
return align_after((void*) region + sizeof(TreeAlloc), actual_align);
}
void *alloc_growable(Arena *arena, uintptr_t size, uintptr_t align) {
// TODO: Basically the same as above, but put the allocated region in the center of the largest free
// space. Due to alignment and whatnot, the code will be gory.
return NULL;
}

33
util.c

@ -1,13 +1,40 @@
#include <stdint.h>
#ifdef DEBUG
#include <stdio.h>
#endif
void* align_after(void* address, int align) {
uintptr_t addr = (uintptr_t) address;
uintptr_t offset = addr % align;
if offset == 0 {
return address;
void *rv;
if (offset == 0) {
rv = address;
} else {
return (void*) (addr + align - align % offset);
rv = (void*) (addr + align - align % offset);
}
#ifdef DEBUG
printf("aligning %p at alignment %x to %p\n", address, align, rv);
#endif
return rv;
}
uintptr_t gcd(uintptr_t a, uintptr_t b) {
if (b > a) {
return gcd(b, a);
}
if (b == 0) {
return a;
}
return gcd(b, a % b);
}
uintptr_t lcm(uintptr_t a, uintptr_t b) {
uintptr_t lcm = (a / gcd(a, b)) * b;
#ifdef DEBUG
printf("so apparently lcm(%lu, %lu) = %lu\n", a, b, lcm);
#endif
return lcm;
}
Loading…
Cancel
Save