Serene Runtime 1.0.0-dev
C runtime for the Serene programming language
Loading...
Searching...
No Matches
default.c
Go to the documentation of this file.
1/* -*- C -*-
2 * Serene programming language
3 * Copyright (C) 2019-2026 Sameer Rahmani <[email protected]>
4 *
5 * This library is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU Lesser General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This library is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU Lesser General Public License for more details.
14 *
15 * You should have received a copy of the GNU Lesser General Public License
16 * along with this library. If not, see <https://www.gnu.org/licenses/>.
17 */
18
19#include <assert.h>
20#include <inttypes.h>
21#include <stdlib.h>
22#include <string.h>
23
25#include "serene/utils.h"
26
27#if defined(_WIN32)
28# include <windows.h>
29#elif defined(__unix__) || defined(__APPLE__)
30# include <unistd.h>
31#elif defined(__wasm__)
32# define WASM_PAGE_SIZE 65536U
33#endif
34
35#if defined(MM_DEBUG)
36# define MM_LOG(FMT, ...) DBG("MM", FMT __VA_OPT__(, ) __VA_ARGS__)
37#else
38# define MM_LOG(FMT, ...)
39#endif
40
41// NOLINTBEGIN(readability-magic-numbers)
42// -----------------------------------------------------------------------------
43// Helper functions
44// -----------------------------------------------------------------------------
45static inline bool is_block_id_free(const srn_mm_t *mm, uint16_t bit) {
46 PANIC_IF(bit >= 256, "Bit position is out of block_bitmap boundaries.");
47
48 // divide by 64, which 64-bit word
49 uint64_t index = bit >> 6U;
50 // remainder, which bit in that word
51 unsigned offset = bit & 63U;
52
53 return ((mm->block_bitmap[index] >> offset) & 1ULL) == 0;
54}
55
56static inline void allocated_block_id(srn_mm_t *mm, uint16_t bit) {
57 PANIC_IF(bit >= 256, "Bit position is out of block_bitmap boundaries.");
58
59 // divide by 64, which 64-bit word
60 uint64_t index = bit >> 6U;
61 // remainder, which bit in that word
62 unsigned offset = bit & 63U;
63
64 mm->block_bitmap[index] |= (1ULL << offset);
65}
66
67static inline void deallocated_block_id(srn_mm_t *mm, uint16_t bit) {
68 PANIC_IF(bit >= 256, "Bit position is out of block_bitmap boundaries.");
69
70 uint64_t index = bit >> 6U;
71 unsigned offset = bit & 63U;
72 uint64_t mask = ~(1ULL << offset);
73 mm->block_bitmap[index] &= mask;
74}
75
76static inline int find_a_free_block_id(const srn_mm_t *mm) {
77#if defined(__GNUC__) || defined(__clang__)
78 for (int i = 0; i < 4; i++) {
79 uint64_t word = mm->block_bitmap[i];
80
81 if (~word) {
82 // invert, then find first 1 bit
83 int bit = __builtin_ctzll(~word);
84 return (i * 64) + bit;
85 }
86 }
87 return -1;
88#else
89 // Fallback
90 for (int i = 0; i < 4; i++) {
91 uint64_t word = mm->block_bitmap[i];
92 if (word != ~0ULL) {
93 uint64_t mask = ~word;
94 int bit = 0;
95 while ((mask & 1ULL) == 0) {
96 mask >>= 1;
97 bit++;
98 }
99 return (i * 64) + bit;
100 }
101 }
102 return -1;
103#endif
104}
105
106/**
107 * An abstraction over ID->Block operation
108 */
109static inline srn_block_t *get_block(const srn_mm_t *mm, srn_block_id_t block_id) {
110 // Ids are reused, so the array slot decides liveness, a released or never
111 // allocated id holds a nullptr.
112 if (block_id < MAX_NUMBER_OF_BLOCKS) {
113 return mm->blocks[block_id];
114 }
115 return nullptr;
116}
117
118/**
119 * Just a proxy functions to the standard stdlib malloc/free
120 * later on if we decided to use mimalloc or something or even
121 * our own malloc, we can plug them in like this
122 */
123static void *stdlib_allocator(size_t size, size_t alignment) {
124 PANIC_IF(size % alignment != 0, "'size' should be a multiple of alignment");
125
126#if defined(_WIN32)
127 return _aligned_malloc(size, alignment);
128#else
129 // C11 standard. It stills bothers me why microsoft is not doing it.
130 return aligned_alloc(alignment, size);
131#endif
132}
133
134static void stdlib_releaser(void *ptr) {
135#if defined(_WIN32)
136 // _aligned_malloc memory must go back through _aligned_free; plain free
137 // corrupts the CRT heap.
138 _aligned_free(ptr);
139#else
140 free(ptr);
141#endif
142}
143
145 .allocate = &stdlib_allocator,
146 .release = &stdlib_releaser,
147};
148
149// ---------------------------------------------------------------------------
150// Generic allocations (currently a thin wrapper over stdlib malloc/realloc
151// /free). Block based machinery above is untouched; these are for callers
152// that need a plain "give me N bytes, here you go" allocator.
153// ---------------------------------------------------------------------------
154
155void *srn_mm_malloc(srn_mm_t *mm, size_t size) {
156 UNUSED(mm);
157 return malloc(size);
158}
159
160void *srn_mm_reallocate(srn_mm_t *mm, void *ptr, size_t new_size) {
161 UNUSED(mm);
162 return realloc(ptr, new_size);
163}
164
165void srn_mm_free(srn_mm_t *mm, void *ptr) {
166 UNUSED(mm);
167 free(ptr);
168}
169
170/**
171 * Return the size of the payload section of the block
172 */
173static inline size_t block_capacity(const srn_block_t *block) {
174 return block->size - offsetof(srn_block_t, base);
175}
176
177/**
178 * Return the number of unused payload bytes left in the block
179 */
180[[maybe_unused]]
181static inline size_t block_remaining(const srn_block_t *block) {
182 return block_capacity(block) - block->offset;
183}
184
185static inline void init_block(srn_mm_t *mm, srn_block_t *block) {
186 PANIC_IF_NULL(mm);
187 block->next = nullptr;
188 block->size = mm->block_size;
189 block->offset = 0;
190}
191
192/**
193 * Allocate a block worth of memory using the memory provider. This is an
194 * internal function and it will NOT allocate block_id nor track the block
195 * via the memory manager by default.
196 */
197static inline void *alloc_block_internal(srn_mm_t *mm) {
198 PANIC_IF_NULL(mm);
199 srn_block_t *block =
201 PANIC_IF_NULL(block);
202 init_block(mm, block);
203 return block;
204}
205
206static inline void destroy_chain(srn_mm_t *mm, srn_block_id_t root_id) {
208
209 // The liveness check happens under the manager lock, so two racing
210 // releases of the same id cannot both pass it and double free the chain.
211 if (is_block_id_free(mm, root_id)) {
213 return;
214 }
215
216 // Holding the chain lock closes the race against an allocation walking
217 // this chain, the walk either finishes before the free, or starts after
218 // the slot is nulled and panics on the dead id.
219 srn_spinlock_t *chain_lock = &mm->chain_locks[root_id];
220 srn_spinlock_lock(chain_lock);
221
222 srn_block_t *block = get_block(mm, root_id);
223 while (block != nullptr) {
224 srn_block_t *tmp = block;
225 block = tmp->next;
226
227 mm->provider->release(tmp);
228 }
229
230 mm->blocks[root_id] = nullptr;
231 mm->block_count--;
232 deallocated_block_id(mm, root_id);
233 srn_spinlock_unlock(chain_lock);
235}
236
237/**
238 * This is the main allocation logic that allocates the space in the given
239 * block. Other public functions have to use this fuction to operate.
240 * The caller must hold the chain's lock for the whole call.
241 */
242static inline void *
243alloc_in_block(srn_mm_t *mm, srn_block_t *root_block, size_t size, size_t alignment) {
244 PANIC_IF(
245 alignment == 0 || (alignment & (alignment - 1)) != 0,
246 "'alignment' must be a nonzero power of two"
247 );
248
249 if (size > block_capacity(root_block) || alignment > block_capacity(root_block)) {
250 // Since all the blocks in a block chain have the same size,
251 // if user is asking for an allocation larger than the block size,
252 // there is no way we can find it in this chain, so a larger block
253 // size is required
254 TODO("We need to allocate a larger block");
255 }
256 MM_LOG("Allocating %zu with %zu alignment", size, alignment);
257
258 MM_LOG("Root block: %p", (void *)root_block);
259 srn_block_t *target_block = root_block;
260 MM_LOG("Walking the block chain");
261 for (;;) {
262 MM_LOG("Next block: %p", (void *)target_block);
263
264 // Align the absolute address, not the offset. The payload base is only
265 // guaranteed DEFAULT_BLOCK_ALIGNMENT, so offset arithmetic alone returns
266 // misaligned pointers for larger alignments.
267 uintptr_t base = (uintptr_t)target_block->base;
268 uintptr_t aligned =
269 (base + target_block->offset + (alignment - 1)) & ~((uintptr_t)alignment - 1);
270 size_t aligned_offset = (size_t)(aligned - base);
271 size_t capacity = block_capacity(target_block);
272
273 MM_LOG("Calculated aligned offset 0x%zx", aligned_offset);
274
275 if (aligned_offset + size > capacity) {
276 MM_LOG("We can't allocate in this block");
277 if (target_block->next != nullptr) {
278 // let's try the next block in the chain
279 target_block = target_block->next;
280 continue;
281 }
282 // We need to create a new block for this chain.
283 MM_LOG("We have to allocate a new block");
284
286 target_block->next = b;
287 target_block = b;
288 MM_LOG("New block: %p", (void *)target_block);
289 continue;
290 }
291 MM_LOG("We have found enough space");
292
293 void *ptr = (void *)aligned;
294 target_block->offset = aligned_offset + size;
295
296 return ptr;
297 }
298}
299
300// -----------------------------------------------------------------------------
301// Public API
302// -----------------------------------------------------------------------------
304#if defined(_WIN32)
305 SYSTEM_INFO si;
306 GetSystemInfo(&si);
307 return (size_t)si.dwPageSize;
308#elif defined(__unix__) || defined(__APPLE__)
309 long sz = sysconf(_SC_PAGESIZE);
310 return (sz > 0) ? (size_t)sz : FALLBACK_PAGE_SIZE;
311#elif defined(__wasm__)
312 return WASM_PAGE_SIZE;
313#else
314 return FALLBACK_PAGE_SIZE;
315#endif
316}
317
319 return get_block(mm, block_id);
320}
321
323 if (config != nullptr) {
324 srn_config_validate(config);
325 }
326
327 srn_mm_t *mm = malloc(sizeof(srn_mm_t));
328 PANIC_IF_NULL(mm);
329
331 memset(mm->block_bitmap, 0, sizeof(mm->block_bitmap));
332
333 // The block size is the one knob the manager reads at init. It comes as a
334 // magnitude, so the size is a power of two and a whole number of pages by
335 // construction, with nothing to round.
336 const size_t magnitude =
338
339 mm->block_count = 0;
340 mm->block_size = (size_t)1 << magnitude;
341 memset((void *)mm->blocks, 0, sizeof(mm->blocks));
342
343 PANIC_IF(
344 mm->block_size <= sizeof(srn_block_t),
345 "Wrong block size. Configure mm.block_size_magnitude to a larger number"
346 );
347 // srn_config_validate only sees a caller provided config, so the default
348 // path is checked here too. The page size is only known at run time.
349 PANIC_IF(
350 mm->block_size % srn_mm_get_os_page_size() != 0, "Block size must be a whole number of OS pages"
351 );
352
354 for (size_t i = 0; i < MAX_NUMBER_OF_BLOCKS; i++) {
356 }
358
359 srn_block_t *immortal =
361 PANIC_IF_NULL(immortal);
362 init_block(mm, immortal);
363 mm->immortal_block = immortal;
364
365#if SERENE_DEBUG
366 mm->stats.allocated_pages = 0;
367 mm->stats.total_allocations = 0;
368 mm->stats.total_os_allocations = 0;
369 mm->stats.total_blocks = 0;
370#endif
371 return mm;
372}
373
375 PANIC_IF_NULL(mm);
376 assert(mm->provider != nullptr);
377
378 for (size_t i = 0; i < MAX_NUMBER_OF_BLOCKS; i++) {
379 if (mm->blocks[i] != nullptr) {
380 destroy_chain(mm, i);
381 }
382 }
383
384 srn_block_t *block = mm->immortal_block;
385
386 while (block != nullptr) {
387 srn_block_t *tmp = block;
388 block = tmp->next;
389
390 mm->provider->release(tmp);
391 }
392
393 mm->provider->release(mm);
394}
395
397 srn_mm_t *mm, srn_block_id_t block_id, size_t size, size_t alignment
398) {
399 MM_LOG("Allocating %zu bytes with %zu bytes alignment in block: %zu", size, alignment, block_id);
400 PANIC_IF(block_id >= MAX_NUMBER_OF_BLOCKS, "Block id out of range");
401
402 // The id resolves to a block only under the chain lock, so a release
403 // cannot free the chain out from under this walk.
404 srn_spinlock_t *chain_lock = &mm->chain_locks[block_id];
405 srn_spinlock_lock(chain_lock);
406
407 srn_block_t *block = get_block(mm, block_id);
408 PANIC_IF_NULL(block);
409
410 void *ptr = alloc_in_block(mm, block, size, alignment);
411 srn_spinlock_unlock(chain_lock);
412 return ptr;
413}
414
415void *srn_mm_immortal_allocate_aligned(srn_mm_t *mm, size_t size, size_t alignment) {
417 void *ptr = alloc_in_block(mm, mm->immortal_block, size, alignment);
419 return ptr;
420}
421
423
425
428 // Ids are reused after release, so exhaustion means every id is live
429 // right now, not that this many allocations ever happened.
430 int index = find_a_free_block_id(mm);
431 PANIC_IF(index == -1, "Out of memory: all block ids are in use");
433
434 mm->block_count++;
435 PANIC_IF(
436 !is_block_id_free(mm, (uint16_t)index),
437 "Miscalculated the block id. It is not free. This is a bug!"
438 );
439 allocated_block_id(mm, (uint16_t)index);
440 mm->blocks[(srn_block_id_t)index] = block;
441
443
444#if SERENE_DEBUG
445 mm->stats.total_blocks++;
446 mm->stats.total_os_allocations++;
447#endif
448 return (srn_block_id_t)index;
449}
450
452 PANIC_IF_NULL(mm);
453 destroy_chain(mm, id);
454}
455
456#if SERENE_DEBUG
457void srn_mm_print_block_summary(srn_mm_t *mm, srn_block_id_t id) {
458 srn_block_t *block = get_block(mm, id);
459 PANIC_IF_NULL(block);
460
461 printf("Block: %zu@%" PRIXPTR "\n", id, (uintptr_t)block);
462 printf("======================================================\n");
463 printf("Next:\t");
464 if (block->next == nullptr) {
465 printf("X\n");
466 } else {
467 printf("%" PRIXPTR "\n", (uintptr_t)block->next);
468 }
469}
470
471void srn_mm_print_blocks_summary(srn_mm_t *mm) {
472 for (size_t d = 0; d < MAX_NUMBER_OF_BLOCKS; d++) {
473 if (mm->blocks[d] != nullptr) {
474 srn_mm_print_block_summary(mm, d);
475 }
476 }
477}
478#endif
479// NOLINTEND(readability-magic-numbers)
void srn_config_validate(const srn_configuration_t *config)
A configuration with every field set to its default.
#define SRN_CONFIG_DEFAULT_BLOCK_SIZE_MAGNITUDE
Magnitude of one memory-manager block.
size_t srn_block_id_t
The block id is effectively just an index in the blocks array in srn_mm_t.
Definition context.h:38
void srn_mm_release_block(srn_mm_t *mm, srn_block_id_t id)
Release the given block id and free the memory for later allocations.
Definition default.c:451
void * srn_mm_allocate_in_block_aligned(srn_mm_t *mm, srn_block_id_t block_id, size_t size, size_t alignment)
Allocate memory on a block with the given block_id.
Definition default.c:396
static void destroy_chain(srn_mm_t *mm, srn_block_id_t root_id)
Definition default.c:206
static void * alloc_block_internal(srn_mm_t *mm)
Allocate a block worth of memory using the memory provider.
Definition default.c:197
static void stdlib_releaser(void *ptr)
Definition default.c:134
#define MM_LOG(FMT,...)
Definition default.c:38
static srn_block_t * get_block(const srn_mm_t *mm, srn_block_id_t block_id)
An abstraction over ID->Block operation.
Definition default.c:109
static void init_block(srn_mm_t *mm, srn_block_t *block)
Definition default.c:185
static srn_memory_provider_t stdlib_provider
Definition default.c:144
static void deallocated_block_id(srn_mm_t *mm, uint16_t bit)
Definition default.c:67
void srn_lock_memory_manager(srn_mm_t *mm)
Locks the memory manager.
Definition default.c:424
static int find_a_free_block_id(const srn_mm_t *mm)
Definition default.c:76
srn_block_t * srn_mm_get_block(srn_mm_t *mm, srn_block_id_t block_id)
Return the block object associated by the given block_id.
Definition default.c:318
void * srn_mm_reallocate(srn_mm_t *mm, void *ptr, size_t new_size)
Definition default.c:160
void * srn_mm_immortal_allocate_aligned(srn_mm_t *mm, size_t size, size_t alignment)
Allocate memory on the importal block which will never gets freed.
Definition default.c:415
void srn_mm_free(srn_mm_t *mm, void *ptr)
Release a pointer previously returned by srn_mm_malloc or srn_mm_reallocate.
Definition default.c:165
srn_block_id_t srn_mm_allocate_block(srn_mm_t *mm)
Allocate a new block in the memory manager and return its ID.
Definition default.c:426
size_t srn_mm_get_os_page_size(void)
Retutrns the OS page size.
Definition default.c:303
static void allocated_block_id(srn_mm_t *mm, uint16_t bit)
Definition default.c:56
void srn_mm_shutdown(srn_mm_t *mm)
Shut down the memory manager and release the resources.
Definition default.c:374
static size_t block_remaining(const srn_block_t *block)
Return the number of unused payload bytes left in the block.
Definition default.c:181
static size_t block_capacity(const srn_block_t *block)
Return the size of the payload section of the block.
Definition default.c:173
srn_mm_t * srn_mm_init(const srn_configuration_t *config)
Initialize the memory manager, this function will panic on error.
Definition default.c:322
void * srn_mm_malloc(srn_mm_t *mm, size_t size)
Generic allocations that do not participate in the block based pools.
Definition default.c:155
static void * alloc_in_block(srn_mm_t *mm, srn_block_t *root_block, size_t size, size_t alignment)
This is the main allocation logic that allocates the space in the given block.
Definition default.c:243
static void * stdlib_allocator(size_t size, size_t alignment)
Just a proxy functions to the standard stdlib malloc/free later on if we decided to use mimalloc or s...
Definition default.c:123
void srn_unlock_memory_manager(srn_mm_t *mm)
Unocks the memory manager.
Definition default.c:422
static bool is_block_id_free(const srn_mm_t *mm, uint16_t bit)
Definition default.c:45
#define FALLBACK_PAGE_SIZE
Definition interface.h:42
#define MAX_NUMBER_OF_BLOCKS
array of blocks is enough for us, we can tweak the size as we see fit.
Definition interface.h:50
#define DEFAULT_BLOCK_ALIGNMENT
We strictly use 16 bytes alignment for blocks.
Definition interface.h:53
size_t offset
Offset from the base.
Definition interface.h:85
size_t size
This is the TOTAL size of the block, header + payloud.
Definition interface.h:82
struct srn_block_t * next
when the block does not have space to allocate a request, we will allocate a new block and point to i...
Definition interface.h:77
Every runtime knob, in one place.
srn_mm_config_t mm
This interface is here to abstract over the allocator.
Definition interface.h:69
void *(* allocate)(size_t size, size_t alignment)
Definition interface.h:70
void(* release)(void *p)
Definition interface.h:71
size_t block_size_magnitude
Magnitude of one block the arena hands out from.
Main memory manager structure that will own all the allocated blocks and data.
Definition interface.h:107
srn_block_t * blocks[MAX_NUMBER_OF_BLOCKS]
Definition interface.h:126
srn_spinlock_t lock
This spinlock is here to protect the srn_mm_t when allocating/deallocating new blocks.
Definition interface.h:114
srn_block_t * immortal_block
Immortal block is a chain of blocks which will never die.
Definition interface.h:139
size_t block_size
Definition interface.h:122
size_t block_count
Number of live chains.
Definition interface.h:125
srn_spinlock_t chain_locks[MAX_NUMBER_OF_BLOCKS]
One lock per chain, keyed by block id.
Definition interface.h:131
uint64_t block_bitmap[4]
This is a 256bit bitmap we treat it as a whole.
Definition interface.h:121
srn_memory_provider_t * provider
An abstraction over a memory provider like the malloc/free pair.
Definition interface.h:117
srn_spinlock_t immortal_lock
The immortal chain has no block id, so it gets its own chain lock.
Definition interface.h:134
#define PANIC_IF_NULL(ptr)
Definition utils.h:66
static void srn_spinlock_lock(srn_spinlock_t *lock)
Definition utils.h:285
#define PANIC_IF(cond, msg)
Definition utils.h:59
static void srn_spinlock_unlock(srn_spinlock_t *lock)
Definition utils.h:276
#define UNUSED(x)
Definition utils.h:45
static void srn_spinlock_init(srn_spinlock_t *lock)
Definition utils.h:280
#define TODO(msg)
Definition utils.h:54