1 // Copyright 2024 The Pigweed Authors
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License"); you may not
4 // use this file except in compliance with the License. You may obtain a copy of
5 // the License at
6 //
7 // https://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11 // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12 // License for the specific language governing permissions and limitations under
13 // the License.
14
15 #include "pw_allocator/buddy_allocator.h"
16
17 #include <cstring>
18 #include <utility>
19
20 #include "lib/stdcompat/bit.h"
21 #include "pw_assert/check.h"
22 #include "pw_bytes/alignment.h"
23
24 namespace pw::allocator::internal {
25
BuddyBlock(size_t outer_size)26 BuddyBlock::BuddyBlock(size_t outer_size) {
27 outer_size_log2_ = cpp20::countr_zero(outer_size);
28 }
29
CanAlloc(Layout layout) const30 StatusWithSize BuddyBlock::CanAlloc(Layout layout) const {
31 return layout.size() > InnerSize() ? StatusWithSize::ResourceExhausted()
32 : StatusWithSize(0);
33 }
34
Split()35 BuddyBlock* BuddyBlock::Split() {
36 outer_size_log2_--;
37 std::byte* ptr = UsableSpace() + InnerSize();
38 return new (ptr) BuddyBlock(OuterSize());
39 }
40
Merge(BuddyBlock * && left,BuddyBlock * && right)41 BuddyBlock* BuddyBlock::Merge(BuddyBlock*&& left, BuddyBlock*&& right) {
42 if (right < left) {
43 return BuddyBlock::Merge(std::move(right), std::move(left));
44 }
45 left->outer_size_log2_++;
46 return left;
47 }
48
GenericBuddyAllocator(span<BucketType> buckets,size_t min_outer_size)49 GenericBuddyAllocator::GenericBuddyAllocator(span<BucketType> buckets,
50 size_t min_outer_size)
51 : buckets_(buckets) {
52 min_outer_size_ = min_outer_size;
53 for (BucketType& bucket : buckets) {
54 size_t max_inner_size = BuddyBlock::InnerSizeFromOuterSize(min_outer_size);
55 bucket.set_max_inner_size(max_inner_size);
56 min_outer_size <<= 1;
57 }
58 }
59
Init(ByteSpan region)60 void GenericBuddyAllocator::Init(ByteSpan region) {
61 CrashIfAllocated();
62
63 // Ensure there is a byte preceding the first `BuddyBlock`.
64 region = GetAlignedSubspan(region.subspan(1), min_outer_size_);
65 region = ByteSpan(region.data() - 1, region.size() + 1);
66 PW_CHECK_INT_GE(region.size(), min_outer_size_);
67
68 // Build up the available memory by successively freeing (and thus merging)
69 // minimum sized blocks.
70 std::byte* data = region.data();
71 size_t count = 0;
72 while (region.size() >= min_outer_size_) {
73 new (region.data()) BuddyBlock(min_outer_size_);
74 region = region.subspan(min_outer_size_);
75 ++count;
76 }
77 region_ = ByteSpan(data, min_outer_size_ * count);
78 data++;
79 for (size_t i = 0; i < count; ++i) {
80 Deallocate(data);
81 data += min_outer_size_;
82 }
83 }
84
CrashIfAllocated()85 void GenericBuddyAllocator::CrashIfAllocated() {
86 size_t total_free = 0;
87 // Drain all the buckets before destroying the list. Although O(n), this
88 // should be reasonably quick since all memory should have been freed and
89 // coalesced prior to calling this method.
90 for (auto& bucket : buckets_) {
91 while (!bucket.empty()) {
92 BuddyBlock* block = bucket.RemoveAny();
93 total_free += block->OuterSize();
94 }
95 }
96 PW_CHECK_INT_EQ(region_.size(),
97 total_free,
98 "%zu bytes were still in use when an allocator was "
99 "destroyed. All memory allocated by an allocator must be "
100 "released before the allocator goes out of scope.",
101 region_.size() - total_free);
102 region_ = ByteSpan();
103 }
104
Allocate(Layout layout)105 void* GenericBuddyAllocator::Allocate(Layout layout) {
106 if (layout.size() > buckets_.back().max_inner_size()) {
107 return nullptr;
108 }
109 if (layout.alignment() > min_outer_size_) {
110 return nullptr;
111 }
112
113 for (auto& bucket : buckets_) {
114 size_t inner_size = bucket.max_inner_size();
115 size_t outer_size = BuddyBlock::OuterSizeFromInnerSize(inner_size);
116 if (inner_size < layout.size()) {
117 continue;
118 }
119 layout = Layout(inner_size, layout.alignment());
120
121 // Check if this bucket has a compatible free block available.
122 if (auto* block = bucket.RemoveCompatible(layout); block != nullptr) {
123 return block->UsableSpace();
124 }
125
126 // No compatible free blocks available, allocate one from the next bucket
127 // and split it.
128 void* ptr = Allocate(layout.Extend(outer_size));
129 if (ptr == nullptr) {
130 break;
131 }
132
133 auto* block = BuddyBlock::FromUsableSpace(ptr);
134 BuddyBlock* buddy = block->Split();
135 std::ignore = bucket.Add(*buddy);
136 return ptr;
137 }
138 return nullptr;
139 }
140
Deallocate(void * ptr)141 void GenericBuddyAllocator::Deallocate(void* ptr) {
142 if (ptr == nullptr) {
143 return;
144 }
145
146 auto* block = BuddyBlock::FromUsableSpace(ptr);
147 BucketType* bucket = nullptr;
148 PW_CHECK_INT_GT(buckets_.size(), 0);
149 for (auto& current : span(buckets_.data(), buckets_.size() - 1)) {
150 size_t outer_size =
151 BuddyBlock::OuterSizeFromInnerSize(current.max_inner_size());
152 if (outer_size < block->OuterSize()) {
153 continue;
154 }
155 bucket = ¤t;
156
157 // Determine the expected address of this free block's buddy by determining
158 // if it would be first or second in a merged block of the next larger size.
159 std::byte* item = block->UsableSpace();
160 if ((item - region_.data()) % (block->OuterSize() * 2) == 0) {
161 item += outer_size;
162 } else {
163 item -= outer_size;
164 }
165 // Blocks at the end of the range may not have a buddy.
166 if (item < region_.data() || region_.data() + region_.size() < item) {
167 break;
168 }
169
170 // Look for the buddy block in the previous bucket. If found, remove it from
171 // that bucket, and repeat the whole process with the merged block.
172 auto* buddy = BuddyBlock::FromUsableSpace(item);
173 if (!current.Remove(*buddy)) {
174 break;
175 }
176
177 block = BuddyBlock::Merge(std::move(block), std::move(buddy));
178 bucket = nullptr;
179 }
180 if (bucket == nullptr) {
181 bucket = &buckets_.back();
182 }
183
184 // Add the (possibly merged) free block to the correct bucket.
185 std::ignore = bucket->Add(*block);
186 }
187
GetLayout(const void * ptr) const188 Result<Layout> GenericBuddyAllocator::GetLayout(const void* ptr) const {
189 if (ptr < region_.data()) {
190 return Status::OutOfRange();
191 }
192 size_t offset = cpp20::bit_cast<uintptr_t>(ptr) -
193 cpp20::bit_cast<uintptr_t>(region_.data());
194 if (region_.size() <= offset || offset % min_outer_size_ != 0) {
195 return Status::OutOfRange();
196 }
197 const auto* block = BuddyBlock::FromUsableSpace(ptr);
198 return Layout(block->InnerSize(), min_outer_size_);
199 }
200
201 } // namespace pw::allocator::internal
202