Skip to content

Commit 3eebeb7

Browse files
authored
[libc] Add aligned_alloc (#96586)
This adds support for aligned_alloc with the freelist allocator. This works by finding blocks large enough to hold the requested size plus some shift amount that's at most the requested alignment. Blocks that meet this requirement but aren't properly aligned can be split such that the usable_space of a new block is aligned properly. The "padding" block created will be merged with the previous block if one exists.
1 parent d3a76b0 commit 3eebeb7

File tree

9 files changed

+501
-11
lines changed

9 files changed

+501
-11
lines changed

libc/src/__support/block.h

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
#include "src/__support/CPP/optional.h"
1717
#include "src/__support/CPP/span.h"
1818
#include "src/__support/CPP/type_traits.h"
19+
#include "src/__support/libc_assert.h"
1920

2021
#include <stdint.h>
2122

@@ -261,6 +262,63 @@ class Block {
261262

262263
constexpr Block(size_t prev_outer_size, size_t outer_size);
263264

265+
bool is_usable_space_aligned(size_t alignment) const {
266+
return reinterpret_cast<uintptr_t>(usable_space()) % alignment == 0;
267+
}
268+
269+
size_t padding_for_alignment(size_t alignment) const {
270+
if (is_usable_space_aligned(alignment))
271+
return 0;
272+
273+
// We need to ensure we can always split this block into a "padding" block
274+
// and the aligned block. To do this, we need enough extra space for at
275+
// least one block.
276+
//
277+
// |block |usable_space |
278+
// |........|......................................|
279+
// ^
280+
// Alignment requirement
281+
//
282+
//
283+
// |block |space |block |usable_space |
284+
// |........|........|........|....................|
285+
// ^
286+
// Alignment requirement
287+
//
288+
uintptr_t start = reinterpret_cast<uintptr_t>(usable_space());
289+
alignment = cpp::max(alignment, ALIGNMENT);
290+
return align_up(start + BLOCK_OVERHEAD, alignment) - start;
291+
}
292+
293+
// Check that we can `allocate` a block with a given alignment and size from
294+
// this existing block.
295+
bool can_allocate(size_t alignment, size_t size) const;
296+
297+
// This is the return type for `allocate` which can split one block into up to
298+
// three blocks.
299+
struct BlockInfo {
300+
// This is the newly aligned block. It will have the alignment requested by
301+
// a call to `allocate` and at most `size`.
302+
Block *block;
303+
304+
// If the usable_space in the new block was not aligned according to the
305+
// `alignment` parameter, we will need to split into this block and the
306+
// `block` to ensure `block` is properly aligned. In this case, `prev` will
307+
// be a pointer to this new "padding" block. `prev` will be nullptr if no
308+
// new block was created or we were able to merge the block before the
309+
// original block with the "padding" block.
310+
Block *prev;
311+
312+
// This is the remainder of the next block after splitting the `block`
313+
// according to `size`. This can happen if there's enough space after the
314+
// `block`.
315+
Block *next;
316+
};
317+
318+
// Divide a block into up to 3 blocks according to `BlockInfo`. This should
319+
// only be called if `can_allocate` returns true.
320+
static BlockInfo allocate(Block *block, size_t alignment, size_t size);
321+
264322
private:
265323
/// Consumes the block and returns as a span of bytes.
266324
static ByteSpan as_bytes(Block *&&block);
@@ -357,6 +415,69 @@ void Block<OffsetType, kAlign>::free(Block *&block) {
357415
merge_next(block);
358416
}
359417

418+
template <typename OffsetType, size_t kAlign>
419+
bool Block<OffsetType, kAlign>::can_allocate(size_t alignment,
420+
size_t size) const {
421+
if (is_usable_space_aligned(alignment) && inner_size() >= size)
422+
return true; // Size and alignment constraints met.
423+
424+
// Either the alignment isn't met or we don't have enough size.
425+
// If we don't meet alignment, we can always adjust such that we do meet the
426+
// alignment. If we meet the alignment but just don't have enough size. This
427+
// check will fail anyway.
428+
size_t adjustment = padding_for_alignment(alignment);
429+
return inner_size() >= size + adjustment;
430+
}
431+
432+
template <typename OffsetType, size_t kAlign>
433+
typename Block<OffsetType, kAlign>::BlockInfo
434+
Block<OffsetType, kAlign>::allocate(Block *block, size_t alignment,
435+
size_t size) {
436+
LIBC_ASSERT(
437+
block->can_allocate(alignment, size) &&
438+
"Calls to this function for a given alignment and size should only be "
439+
"done if `can_allocate` for these parameters returns true.");
440+
441+
BlockInfo info{block, /*prev=*/nullptr, /*next=*/nullptr};
442+
443+
if (!info.block->is_usable_space_aligned(alignment)) {
444+
size_t adjustment = info.block->padding_for_alignment(alignment);
445+
size_t new_inner_size = adjustment - BLOCK_OVERHEAD;
446+
LIBC_ASSERT(new_inner_size % ALIGNMENT == 0 &&
447+
"The adjustment calculation should always return a new size "
448+
"that's a multiple of ALIGNMENT");
449+
450+
Block *original = info.block;
451+
optional<Block *> maybe_aligned_block =
452+
Block::split(original, adjustment - BLOCK_OVERHEAD);
453+
LIBC_ASSERT(maybe_aligned_block.has_value() &&
454+
"This split should always result in a new block. The check in "
455+
"`can_allocate` ensures that we have enough space here to make "
456+
"two blocks.");
457+
458+
if (Block *prev = original->prev()) {
459+
// If there is a block before this, we can merge the current one with the
460+
// newly created one.
461+
merge_next(prev);
462+
} else {
463+
// Otherwise, this was the very first block in the chain. Now we can make
464+
// it the new first block.
465+
info.prev = original;
466+
}
467+
468+
Block *aligned_block = *maybe_aligned_block;
469+
LIBC_ASSERT(aligned_block->is_usable_space_aligned(alignment) &&
470+
"The aligned block isn't aligned somehow.");
471+
info.block = aligned_block;
472+
}
473+
474+
// Now get a block for the requested size.
475+
if (optional<Block *> next = Block::split(info.block, size))
476+
info.next = *next;
477+
478+
return info;
479+
}
480+
360481
template <typename OffsetType, size_t kAlign>
361482
optional<Block<OffsetType, kAlign> *>
362483
Block<OffsetType, kAlign>::split(Block *&block, size_t new_inner_size) {

libc/src/__support/freelist.h

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ template <size_t NUM_BUCKETS = 6> class FreeList {
6666
/// A span with a size of 0.
6767
cpp::span<cpp::byte> find_chunk(size_t size) const;
6868

69+
template <typename Cond> cpp::span<cpp::byte> find_chunk_if(Cond op) const;
70+
6971
/// Removes a chunk from this freelist.
7072
bool remove_chunk(cpp::span<cpp::byte> chunk);
7173

@@ -111,6 +113,22 @@ bool FreeList<NUM_BUCKETS>::add_chunk(span<cpp::byte> chunk) {
111113
return true;
112114
}
113115

116+
template <size_t NUM_BUCKETS>
117+
template <typename Cond>
118+
span<cpp::byte> FreeList<NUM_BUCKETS>::find_chunk_if(Cond op) const {
119+
for (FreeListNode *node : chunks_) {
120+
while (node != nullptr) {
121+
span<cpp::byte> chunk(reinterpret_cast<cpp::byte *>(node), node->size);
122+
if (op(chunk))
123+
return chunk;
124+
125+
node = node->next;
126+
}
127+
}
128+
129+
return {};
130+
}
131+
114132
template <size_t NUM_BUCKETS>
115133
span<cpp::byte> FreeList<NUM_BUCKETS>::find_chunk(size_t size) const {
116134
if (size == 0)

libc/src/__support/freelist_heap.h

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ namespace LIBC_NAMESPACE {
2424
using cpp::optional;
2525
using cpp::span;
2626

27+
inline constexpr bool IsPow2(size_t x) { return x && (x & (x - 1)) == 0; }
28+
2729
static constexpr cpp::array<size_t, 6> DEFAULT_BUCKETS{16, 32, 64,
2830
128, 256, 512};
2931

@@ -32,6 +34,9 @@ template <size_t NUM_BUCKETS = DEFAULT_BUCKETS.size()> class FreeListHeap {
3234
using BlockType = Block<>;
3335
using FreeListType = FreeList<NUM_BUCKETS>;
3436

37+
static constexpr size_t MIN_ALIGNMENT =
38+
cpp::max(BlockType::ALIGNMENT, alignof(max_align_t));
39+
3540
struct HeapStats {
3641
size_t total_bytes;
3742
size_t bytes_allocated;
@@ -55,6 +60,9 @@ template <size_t NUM_BUCKETS = DEFAULT_BUCKETS.size()> class FreeListHeap {
5560
}
5661

5762
void *allocate(size_t size);
63+
void *aligned_allocate(size_t alignment, size_t size);
64+
// NOTE: All pointers passed to free must come from one of the other
65+
// allocation functions: `allocate`, `aligned_allocate`, `realloc`, `calloc`.
5866
void free(void *ptr);
5967
void *realloc(void *ptr, size_t size);
6068
void *calloc(size_t num, size_t size);
@@ -74,6 +82,8 @@ template <size_t NUM_BUCKETS = DEFAULT_BUCKETS.size()> class FreeListHeap {
7482
freelist_.set_freelist_node(node, chunk);
7583
}
7684

85+
void *allocate_impl(size_t alignment, size_t size);
86+
7787
private:
7888
span<cpp::byte> block_to_span(BlockType *block) {
7989
return span<cpp::byte>(block->usable_space(), block->inner_size());
@@ -109,20 +119,31 @@ struct FreeListHeapBuffer : public FreeListHeap<NUM_BUCKETS> {
109119
};
110120

111121
template <size_t NUM_BUCKETS>
112-
void *FreeListHeap<NUM_BUCKETS>::allocate(size_t size) {
113-
// Find a chunk in the freelist. Split it if needed, then return
114-
auto chunk = freelist_.find_chunk(size);
122+
void *FreeListHeap<NUM_BUCKETS>::allocate_impl(size_t alignment, size_t size) {
123+
if (size == 0)
124+
return nullptr;
125+
126+
// Find a chunk in the freelist. Split it if needed, then return.
127+
auto chunk =
128+
freelist_.find_chunk_if([alignment, size](span<cpp::byte> chunk) {
129+
BlockType *block = BlockType::from_usable_space(chunk.data());
130+
return block->can_allocate(alignment, size);
131+
});
115132

116133
if (chunk.data() == nullptr)
117134
return nullptr;
118135
freelist_.remove_chunk(chunk);
119136

120137
BlockType *chunk_block = BlockType::from_usable_space(chunk.data());
138+
LIBC_ASSERT(!chunk_block->used());
121139

122140
// Split that chunk. If there's a leftover chunk, add it to the freelist
123-
optional<BlockType *> result = BlockType::split(chunk_block, size);
124-
if (result)
125-
freelist_.add_chunk(block_to_span(*result));
141+
auto block_info = BlockType::allocate(chunk_block, alignment, size);
142+
if (block_info.next)
143+
freelist_.add_chunk(block_to_span(block_info.next));
144+
if (block_info.prev)
145+
freelist_.add_chunk(block_to_span(block_info.prev));
146+
chunk_block = block_info.block;
126147

127148
chunk_block->mark_used();
128149

@@ -133,6 +154,25 @@ void *FreeListHeap<NUM_BUCKETS>::allocate(size_t size) {
133154
return chunk_block->usable_space();
134155
}
135156

157+
template <size_t NUM_BUCKETS>
158+
void *FreeListHeap<NUM_BUCKETS>::allocate(size_t size) {
159+
return allocate_impl(MIN_ALIGNMENT, size);
160+
}
161+
162+
template <size_t NUM_BUCKETS>
163+
void *FreeListHeap<NUM_BUCKETS>::aligned_allocate(size_t alignment,
164+
size_t size) {
165+
// The alignment must be an integral power of two.
166+
if (!IsPow2(alignment))
167+
return nullptr;
168+
169+
// The size parameter must be an integral multiple of alignment.
170+
if (size % alignment != 0)
171+
return nullptr;
172+
173+
return allocate_impl(alignment, size);
174+
}
175+
136176
template <size_t NUM_BUCKETS> void FreeListHeap<NUM_BUCKETS>::free(void *ptr) {
137177
cpp::byte *bytes = static_cast<cpp::byte *>(ptr);
138178

libc/src/stdlib/aligned_alloc.h

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
//===-- Implementation header for aligned_alloc -----------------*- C++ -*-===//
2+
//
3+
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4+
// See https://llvm.org/LICENSE.txt for license information.
5+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6+
//
7+
//===----------------------------------------------------------------------===//
8+
9+
#include <stddef.h>
10+
11+
#ifndef LLVM_LIBC_SRC_STDLIB_ALIGNED_ALLOC_H
12+
#define LLVM_LIBC_SRC_STDLIB_ALIGNED_ALLOC_H
13+
14+
namespace LIBC_NAMESPACE {
15+
16+
void *aligned_alloc(size_t alignment, size_t size);
17+
18+
} // namespace LIBC_NAMESPACE
19+
20+
#endif // LLVM_LIBC_SRC_STDLIB_ALIGNED_ALLOC_H

libc/src/stdlib/freelist_malloc.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
//===----------------------------------------------------------------------===//
88

99
#include "src/__support/freelist_heap.h"
10+
#include "src/stdlib/aligned_alloc.h"
1011
#include "src/stdlib/calloc.h"
1112
#include "src/stdlib/free.h"
1213
#include "src/stdlib/malloc.h"
@@ -42,4 +43,8 @@ LLVM_LIBC_FUNCTION(void *, realloc, (void *ptr, size_t size)) {
4243
return freelist_heap->realloc(ptr, size);
4344
}
4445

46+
LLVM_LIBC_FUNCTION(void *, aligned_alloc, (size_t alignment, size_t size)) {
47+
return freelist_heap->aligned_allocate(alignment, size);
48+
}
49+
4550
} // namespace LIBC_NAMESPACE

libc/test/src/__support/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ add_libc_test(
88
block_test.cpp
99
DEPENDS
1010
libc.src.__support.CPP.array
11+
libc.src.__support.CPP.bit
1112
libc.src.__support.CPP.span
1213
libc.src.__support.block
1314
libc.src.string.memcpy

0 commit comments

Comments
 (0)