Skip to content

Commit 330aaa6

Browse files
committed
[libc] Use best-fit binary trie to make malloc logarithmic
This reworks the free store implementation in libc's malloc to use a dlmalloc-style binary trie of circularly linked FIFO free lists. This data structure can be maintained in logarithmic time, but it still permits a relatively small implementation compared to other logarithmic-time ordered maps. The implementation doesn't do the various bitwise tricks or optimizations used in actual dlmalloc; it instead optimizes for (relative) readability and minimum code size. Specific optimization can be added as necessary given future profiling.
1 parent cf9d1c1 commit 330aaa6

File tree

13 files changed

+780
-408
lines changed

13 files changed

+780
-408
lines changed

libc/config/linux/x86_64/entrypoints.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ set(TARGET_LIBC_ENTRYPOINTS
202202
libc.src.stdlib.aligned_alloc
203203
libc.src.stdlib.calloc
204204
libc.src.stdlib.free
205+
libc.src.stdlib.freelist_malloc
205206
libc.src.stdlib.malloc
206207
libc.src.stdlib.realloc
207208

libc/src/__support/block.h

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,16 +174,32 @@ class Block {
174174
return inner_size - sizeof(prev_) + BLOCK_OVERHEAD;
175175
}
176176

177-
/// @returns The number of usable bytes inside the block.
177+
/// @returns The number of usable bytes inside the block were it to be
178+
/// allocated.
178179
size_t inner_size() const {
179180
if (!next())
180181
return 0;
181182
return inner_size(outer_size());
182183
}
183184

185+
/// @returns The number of usable bytes inside a block with the given outer
186+
/// size were it to be allocated.
184187
static size_t inner_size(size_t outer_size) {
185188
// The usable region includes the prev_ field of the next block.
186-
return outer_size - BLOCK_OVERHEAD + sizeof(prev_);
189+
return inner_size_free(outer_size) + sizeof(prev_);
190+
}
191+
192+
/// @returns The number of usable bytes inside the block if it remains free.
193+
size_t inner_size_free() const {
194+
if (!next())
195+
return 0;
196+
return inner_size_free(outer_size());
197+
}
198+
199+
/// @returns The number of usable bytes inside a block with the given outer
200+
/// size if it remains free.
201+
static size_t inner_size_free(size_t outer_size) {
202+
return outer_size - BLOCK_OVERHEAD;
187203
}
188204

189205
/// @returns A pointer to the usable space inside this block.

libc/src/__support/freelist.h

Lines changed: 77 additions & 169 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//===-- Interface for freelist_malloc -------------------------------------===//
1+
//===-- Interface for freelist --------------------------------------------===//
22
//
33
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
44
// See https://llvm.org/LICENSE.txt for license information.
@@ -9,199 +9,107 @@
99
#ifndef LLVM_LIBC_SRC___SUPPORT_FREELIST_H
1010
#define LLVM_LIBC_SRC___SUPPORT_FREELIST_H
1111

12-
#include "src/__support/CPP/array.h"
13-
#include "src/__support/CPP/cstddef.h"
14-
#include "src/__support/CPP/new.h"
15-
#include "src/__support/CPP/span.h"
16-
#include "src/__support/fixedvector.h"
17-
#include "src/__support/macros/config.h"
12+
#include "block.h"
1813

1914
namespace LIBC_NAMESPACE_DECL {
2015

21-
using cpp::span;
22-
23-
/// Basic [freelist](https://en.wikipedia.org/wiki/Free_list) implementation
24-
/// for an allocator. This implementation buckets by chunk size, with a list
25-
/// of user-provided buckets. Each bucket is a linked list of storage chunks.
26-
/// Because this freelist uses the added chunks themselves as list nodes, there
27-
/// is a lower bound of `sizeof(FreeList.FreeListNode)` bytes for chunks which
28-
/// can be added to this freelist. There is also an implicit bucket for
29-
/// "everything else", for chunks which do not fit into a bucket.
30-
///
31-
/// Each added chunk will be added to the smallest bucket under which it fits.
32-
/// If it does not fit into any user-provided bucket, it will be added to the
33-
/// default bucket.
34-
///
35-
/// As an example, assume that the `FreeList` is configured with buckets of
36-
/// sizes {64, 128, 256, and 512} bytes. The internal state may look like the
37-
/// following:
38-
///
39-
/// @code{.unparsed}
40-
/// bucket[0] (64B) --> chunk[12B] --> chunk[42B] --> chunk[64B] --> NULL
41-
/// bucket[1] (128B) --> chunk[65B] --> chunk[72B] --> NULL
42-
/// bucket[2] (256B) --> NULL
43-
/// bucket[3] (512B) --> chunk[312B] --> chunk[512B] --> chunk[416B] --> NULL
44-
/// bucket[4] (implicit) --> chunk[1024B] --> chunk[513B] --> NULL
45-
/// @endcode
16+
/// A circularly-linked FIFO list storing free Blocks. All Blocks on a list
17+
/// are the same size.
4618
///
47-
/// Note that added chunks should be aligned to a 4-byte boundary.
48-
template <size_t NUM_BUCKETS = 6> class FreeList {
19+
/// Allocating free blocks in FIFO order maximizes the amount of time before a
20+
/// free block is reused. This in turn maximizes the number of opportunities for
21+
/// it to be coalesced with an adjacent block, which tends to reduce heap
22+
/// fragmentation.
23+
class FreeList {
4924
public:
50-
// Remove copy/move ctors
51-
FreeList(const FreeList &other) = delete;
52-
FreeList(FreeList &&other) = delete;
53-
FreeList &operator=(const FreeList &other) = delete;
54-
FreeList &operator=(FreeList &&other) = delete;
55-
56-
/// Adds a chunk to this freelist.
57-
bool add_chunk(cpp::span<cpp::byte> chunk);
58-
59-
/// Finds an eligible chunk for an allocation of size `size`.
60-
///
61-
/// @note This returns the first allocation possible within a given bucket;
62-
/// It does not currently optimize for finding the smallest chunk.
63-
///
64-
/// @returns
65-
/// * On success - A span representing the chunk.
66-
/// * On failure (e.g. there were no chunks available for that allocation) -
67-
/// A span with a size of 0.
68-
cpp::span<cpp::byte> find_chunk(size_t size) const;
69-
70-
template <typename Cond> cpp::span<cpp::byte> find_chunk_if(Cond op) const;
71-
72-
/// Removes a chunk from this freelist.
73-
bool remove_chunk(cpp::span<cpp::byte> chunk);
74-
75-
/// For a given size, find which index into chunks_ the node should be written
76-
/// to.
77-
constexpr size_t find_chunk_ptr_for_size(size_t size, bool non_null) const;
78-
79-
struct FreeListNode {
80-
FreeListNode *next;
81-
size_t size;
82-
};
83-
84-
constexpr void set_freelist_node(FreeListNode &node,
85-
cpp::span<cpp::byte> chunk);
86-
87-
constexpr explicit FreeList(const cpp::array<size_t, NUM_BUCKETS> &sizes)
88-
: chunks_(NUM_BUCKETS + 1, 0), sizes_(sizes.begin(), sizes.end()) {}
89-
90-
private:
91-
FixedVector<FreeList::FreeListNode *, NUM_BUCKETS + 1> chunks_;
92-
FixedVector<size_t, NUM_BUCKETS> sizes_;
93-
};
94-
95-
template <size_t NUM_BUCKETS>
96-
constexpr void FreeList<NUM_BUCKETS>::set_freelist_node(FreeListNode &node,
97-
span<cpp::byte> chunk) {
98-
// Add it to the correct list.
99-
size_t chunk_ptr = find_chunk_ptr_for_size(chunk.size(), false);
100-
node.size = chunk.size();
101-
node.next = chunks_[chunk_ptr];
102-
chunks_[chunk_ptr] = &node;
103-
}
25+
class Node {
26+
public:
27+
/// @returns The block containing this node.
28+
Block<> *block() const {
29+
return const_cast<Block<> *>(Block<>::from_usable_space(this));
30+
}
10431

105-
template <size_t NUM_BUCKETS>
106-
bool FreeList<NUM_BUCKETS>::add_chunk(span<cpp::byte> chunk) {
107-
// Check that the size is enough to actually store what we need
108-
if (chunk.size() < sizeof(FreeListNode))
109-
return false;
32+
/// @returns The inner size of blocks in the list containing this node.
33+
size_t size() const { return block()->inner_size(); }
11034

111-
FreeListNode *node = ::new (chunk.data()) FreeListNode;
112-
set_freelist_node(*node, chunk);
35+
private:
36+
// Circularly linked pointers to adjacent nodes.
37+
Node *prev;
38+
Node *next;
39+
friend class FreeList;
40+
};
11341

114-
return true;
115-
}
42+
constexpr FreeList() : FreeList(nullptr) {}
43+
constexpr FreeList(Node *begin) : begin_(begin) {}
11644

117-
template <size_t NUM_BUCKETS>
118-
template <typename Cond>
119-
span<cpp::byte> FreeList<NUM_BUCKETS>::find_chunk_if(Cond op) const {
120-
for (FreeListNode *node : chunks_) {
121-
while (node != nullptr) {
122-
span<cpp::byte> chunk(reinterpret_cast<cpp::byte *>(node), node->size);
123-
if (op(chunk))
124-
return chunk;
45+
bool empty() const { return !begin_; }
12546

126-
node = node->next;
127-
}
47+
/// @returns The inner size of blocks in the list.
48+
size_t size() const {
49+
LIBC_ASSERT(begin_ && "empty lists have no size");
50+
return begin_->size();
12851
}
12952

130-
return {};
131-
}
53+
/// @returns The first node in the list.
54+
Node *begin() { return begin_; }
13255

133-
template <size_t NUM_BUCKETS>
134-
span<cpp::byte> FreeList<NUM_BUCKETS>::find_chunk(size_t size) const {
135-
if (size == 0)
136-
return span<cpp::byte>();
56+
/// @returns The first block in the list.
57+
Block<> *front() { return begin_->block(); }
13758

138-
size_t chunk_ptr = find_chunk_ptr_for_size(size, true);
59+
/// Push a block to the back of the list.
60+
/// The block must be large enough to contain a node.
61+
void push(Block<> *block);
13962

140-
// Check that there's data. This catches the case where we run off the
141-
// end of the array
142-
if (chunks_[chunk_ptr] == nullptr)
143-
return span<cpp::byte>();
63+
/// Push an already-constructed node to the back of the list.
64+
/// This allows pushing derived node types with additional data.
65+
void push(Node *node);
14466

145-
// Now iterate up the buckets, walking each list to find a good candidate
146-
for (size_t i = chunk_ptr; i < chunks_.size(); i++) {
147-
FreeListNode *node = chunks_[static_cast<unsigned short>(i)];
67+
/// Pop the first node from the list.
68+
void pop();
14869

149-
while (node != nullptr) {
150-
if (node->size >= size)
151-
return span<cpp::byte>(reinterpret_cast<cpp::byte *>(node), node->size);
70+
/// Remove an arbitrary node from the list.
71+
void remove(Node *node);
15272

153-
node = node->next;
154-
}
155-
}
73+
private:
74+
Node *begin_;
75+
};
15676

157-
// If we get here, we've checked every block in every bucket. There's
158-
// nothing that can support this allocation.
159-
return span<cpp::byte>();
77+
LIBC_INLINE void FreeList::push(Block<> *block) {
78+
LIBC_ASSERT(!block->used() && "only free blocks can be placed on free lists");
79+
LIBC_ASSERT(block->inner_size_free() >= sizeof(FreeList) &&
80+
"block too small to accomodate free list node");
81+
push(new (block->usable_space()) Node);
16082
}
16183

162-
template <size_t NUM_BUCKETS>
163-
bool FreeList<NUM_BUCKETS>::remove_chunk(span<cpp::byte> chunk) {
164-
size_t chunk_ptr = find_chunk_ptr_for_size(chunk.size(), true);
165-
166-
// Check head first.
167-
if (chunks_[chunk_ptr] == nullptr)
168-
return false;
169-
170-
FreeListNode *node = chunks_[chunk_ptr];
171-
if (reinterpret_cast<cpp::byte *>(node) == chunk.data()) {
172-
chunks_[chunk_ptr] = node->next;
173-
return true;
84+
LIBC_INLINE void FreeList::push(Node *node) {
85+
if (begin_) {
86+
LIBC_ASSERT(Block<>::from_usable_space(node)->outer_size() ==
87+
begin_->block()->outer_size() &&
88+
"freelist entries must have the same size");
89+
// Since the list is circular, insert the node immediately before begin_.
90+
node->prev = begin_->prev;
91+
node->next = begin_;
92+
begin_->prev->next = node;
93+
begin_->prev = node;
94+
} else {
95+
begin_ = node->prev = node->next = node;
17496
}
175-
176-
// No? Walk the nodes.
177-
node = chunks_[chunk_ptr];
178-
179-
while (node->next != nullptr) {
180-
if (reinterpret_cast<cpp::byte *>(node->next) == chunk.data()) {
181-
// Found it, remove this node out of the chain
182-
node->next = node->next->next;
183-
return true;
184-
}
185-
186-
node = node->next;
187-
}
188-
189-
return false;
19097
}
19198

192-
template <size_t NUM_BUCKETS>
193-
constexpr size_t
194-
FreeList<NUM_BUCKETS>::find_chunk_ptr_for_size(size_t size,
195-
bool non_null) const {
196-
size_t chunk_ptr = 0;
197-
for (chunk_ptr = 0u; chunk_ptr < sizes_.size(); chunk_ptr++) {
198-
if (sizes_[chunk_ptr] >= size &&
199-
(!non_null || chunks_[chunk_ptr] != nullptr)) {
200-
break;
201-
}
99+
LIBC_INLINE void FreeList::pop() { remove(begin_); }
100+
101+
LIBC_INLINE void FreeList::remove(Node *node) {
102+
LIBC_ASSERT(begin_ && "cannot remove from empty list");
103+
if (node == node->next) {
104+
LIBC_ASSERT(node == begin_ &&
105+
"a self-referential node must be the only element");
106+
begin_ = nullptr;
107+
} else {
108+
node->prev->next = node->next;
109+
node->next->prev = node->prev;
110+
if (begin_ == node)
111+
begin_ = node->next;
202112
}
203-
204-
return chunk_ptr;
205113
}
206114

207115
} // namespace LIBC_NAMESPACE_DECL

0 commit comments

Comments
 (0)