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 void *ptr = malloc(size);
158 MM_TRACEPOINT(mm_malloc, (uint64_t)size, ptr);
159 return ptr;
160}
161
162void *srn_mm_reallocate(srn_mm_t *mm, void *ptr, size_t new_size) {
163 UNUSED(mm);
164 void *out = realloc(ptr, new_size);
165 MM_TRACEPOINT(mm_realloc, ptr, (uint64_t)new_size, out);
166 return out;
167}
168
169void srn_mm_free(srn_mm_t *mm, void *ptr) {
170 UNUSED(mm);
171 MM_TRACEPOINT(mm_free, ptr);
172 free(ptr);
173}
174
175/**
176 * Return the size of the payload section of the block
177 */
178static inline size_t block_capacity(const srn_block_t *block) {
179 return block->size - offsetof(srn_block_t, base);
180}
181
182/**
183 * Return the number of unused payload bytes left in the block
184 */
185[[maybe_unused]]
186static inline size_t block_remaining(const srn_block_t *block) {
187 return block_capacity(block) - block->offset;
188}
189
190static inline void init_block(srn_mm_t *mm, srn_block_t *block) {
191 PANIC_IF_NULL(mm);
192 block->next = nullptr;
193 block->size = mm->block_size;
194 block->offset = 0;
195}
196
197/**
198 * Allocate a block worth of memory using the memory provider. This is an
199 * internal function and it will NOT allocate block_id nor track the block
200 * via the memory manager by default.
201 */
202static inline void *alloc_block_internal(srn_mm_t *mm) {
203 PANIC_IF_NULL(mm);
204 srn_block_t *block =
206 PANIC_IF_NULL(block);
207 init_block(mm, block);
208 MM_TRACEPOINT(mm_block_new, (void *)block, (uint64_t)mm->block_size);
209 return block;
210}
211
212static inline void destroy_chain(srn_mm_t *mm, srn_block_id_t root_id) {
214
215 // The liveness check happens under the manager lock, so two racing
216 // releases of the same id cannot both pass it and double free the chain.
217 if (is_block_id_free(mm, root_id)) {
219 return;
220 }
221
222 // Holding the chain lock closes the race against an allocation walking
223 // this chain, the walk either finishes before the free, or starts after
224 // the slot is nulled and panics on the dead id.
225 srn_spinlock_t *chain_lock = &mm->chain_locks[root_id];
226 srn_spinlock_lock(chain_lock);
227
228 srn_block_t *block = get_block(mm, root_id);
229 size_t freed = 0;
230 while (block != nullptr) {
231 srn_block_t *tmp = block;
232 block = tmp->next;
233
234 mm->provider->release(tmp);
235 freed++;
236 }
237
238 mm->blocks[root_id] = nullptr;
239 mm->block_count--;
240 deallocated_block_id(mm, root_id);
241 MM_TRACEPOINT(mm_chain_free, (uint64_t)root_id, (uint64_t)freed);
242 srn_spinlock_unlock(chain_lock);
244}
245
246/**
247 * This is the main allocation logic that allocates the space in the given
248 * block. Other public functions have to use this fuction to operate.
249 * The caller must hold the chain's lock for the whole call.
250 */
251static inline void *
252alloc_in_block(srn_mm_t *mm, srn_block_t *root_block, size_t size, size_t alignment) {
253 PANIC_IF(
254 alignment == 0 || (alignment & (alignment - 1)) != 0,
255 "'alignment' must be a nonzero power of two"
256 );
257
258 if (size > block_capacity(root_block) || alignment > block_capacity(root_block)) {
259 // Since all the blocks in a block chain have the same size,
260 // if user is asking for an allocation larger than the block size,
261 // there is no way we can find it in this chain, so a larger block
262 // size is required
263 TODO("We need to allocate a larger block");
264 }
265 MM_LOG("Allocating %zu with %zu alignment", size, alignment);
266
267 MM_LOG("Root block: %p", (void *)root_block);
268 srn_block_t *target_block = root_block;
269 MM_LOG("Walking the block chain");
270 for (;;) {
271 MM_LOG("Next block: %p", (void *)target_block);
272
273 // Align the absolute address, not the offset. The payload base is only
274 // guaranteed DEFAULT_BLOCK_ALIGNMENT, so offset arithmetic alone returns
275 // misaligned pointers for larger alignments.
276 uintptr_t base = (uintptr_t)target_block->base;
277 uintptr_t aligned =
278 (base + target_block->offset + (alignment - 1)) & ~((uintptr_t)alignment - 1);
279 size_t aligned_offset = (size_t)(aligned - base);
280 size_t capacity = block_capacity(target_block);
281
282 MM_LOG("Calculated aligned offset 0x%zx", aligned_offset);
283
284 if (aligned_offset + size > capacity) {
285 MM_LOG("We can't allocate in this block");
286 if (target_block->next != nullptr) {
287 // let's try the next block in the chain
288 target_block = target_block->next;
289 continue;
290 }
291 // We need to create a new block for this chain.
292 MM_LOG("We have to allocate a new block");
293
295 target_block->next = b;
296 target_block = b;
297 MM_TRACEPOINT(mm_chain_grow, (void *)root_block, (void *)b);
298 MM_LOG("New block: %p", (void *)target_block);
299 continue;
300 }
301 MM_LOG("We have found enough space");
302
303 void *ptr = (void *)aligned;
304 target_block->offset = aligned_offset + size;
305
306 return ptr;
307 }
308}
309
310// -----------------------------------------------------------------------------
311// Public API
312// -----------------------------------------------------------------------------
314#if defined(_WIN32)
315 SYSTEM_INFO si;
316 GetSystemInfo(&si);
317 return (size_t)si.dwPageSize;
318#elif defined(__unix__) || defined(__APPLE__)
319 long sz = sysconf(_SC_PAGESIZE);
320 return (sz > 0) ? (size_t)sz : FALLBACK_PAGE_SIZE;
321#elif defined(__wasm__)
322 return WASM_PAGE_SIZE;
323#else
324 return FALLBACK_PAGE_SIZE;
325#endif
326}
327
329 return get_block(mm, block_id);
330}
331
333 if (config != nullptr) {
334 srn_config_validate(config);
335 }
336
337 srn_mm_t *mm = malloc(sizeof(srn_mm_t));
338 PANIC_IF_NULL(mm);
339
341 memset(mm->block_bitmap, 0, sizeof(mm->block_bitmap));
342
343 // The block size is the one knob the manager reads at init. It comes as a
344 // magnitude, so the size is a power of two and a whole number of pages by
345 // construction, with nothing to round.
346 const size_t magnitude =
348
349 mm->block_count = 0;
350 mm->block_size = (size_t)1 << magnitude;
351 memset((void *)mm->blocks, 0, sizeof(mm->blocks));
352
353 PANIC_IF(
354 mm->block_size <= sizeof(srn_block_t),
355 "Wrong block size. Configure mm.block_size_magnitude to a larger number"
356 );
357 // srn_config_validate only sees a caller provided config, so the default
358 // path is checked here too. The page size is only known at run time.
359 PANIC_IF(
360 mm->block_size % srn_mm_get_os_page_size() != 0, "Block size must be a whole number of OS pages"
361 );
362
364 for (size_t i = 0; i < MAX_NUMBER_OF_BLOCKS; i++) {
366 }
368
369 srn_block_t *immortal =
371 PANIC_IF_NULL(immortal);
372 init_block(mm, immortal);
373 mm->immortal_block = immortal;
374
375#if SERENE_DEBUG
376 mm->stats.allocated_pages = 0;
377 mm->stats.total_allocations = 0;
378 mm->stats.total_os_allocations = 0;
379 mm->stats.total_blocks = 0;
380#endif
381 return mm;
382}
383
385 PANIC_IF_NULL(mm);
386 assert(mm->provider != nullptr);
387
388 for (size_t i = 0; i < MAX_NUMBER_OF_BLOCKS; i++) {
389 if (mm->blocks[i] != nullptr) {
390 destroy_chain(mm, i);
391 }
392 }
393
394 srn_block_t *block = mm->immortal_block;
395
396 while (block != nullptr) {
397 srn_block_t *tmp = block;
398 block = tmp->next;
399
400 mm->provider->release(tmp);
401 }
402
403 mm->provider->release(mm);
404}
405
407 srn_mm_t *mm, srn_block_id_t block_id, size_t size, size_t alignment
408) {
409 MM_LOG("Allocating %zu bytes with %zu bytes alignment in block: %zu", size, alignment, block_id);
410 PANIC_IF(block_id >= MAX_NUMBER_OF_BLOCKS, "Block id out of range");
411
412 // The id resolves to a block only under the chain lock, so a release
413 // cannot free the chain out from under this walk.
414 srn_spinlock_t *chain_lock = &mm->chain_locks[block_id];
415 srn_spinlock_lock(chain_lock);
416
417 srn_block_t *block = get_block(mm, block_id);
418 PANIC_IF_NULL(block);
419
420 void *ptr = alloc_in_block(mm, block, size, alignment);
421 srn_spinlock_unlock(chain_lock);
422 MM_TRACEPOINT(mm_alloc, (uint64_t)block_id, (uint64_t)size, ptr);
423 return ptr;
424}
425
426void *srn_mm_immortal_allocate_aligned(srn_mm_t *mm, size_t size, size_t alignment) {
428 void *ptr = alloc_in_block(mm, mm->immortal_block, size, alignment);
430 MM_TRACEPOINT(mm_immortal_alloc, (uint64_t)size, ptr);
431 return ptr;
432}
433
435
437
440 // Ids are reused after release, so exhaustion means every id is live
441 // right now, not that this many allocations ever happened.
442 int index = find_a_free_block_id(mm);
443 PANIC_IF(index == -1, "Out of memory: all block ids are in use");
445
446 mm->block_count++;
447 PANIC_IF(
448 !is_block_id_free(mm, (uint16_t)index),
449 "Miscalculated the block id. It is not free. This is a bug!"
450 );
451 allocated_block_id(mm, (uint16_t)index);
452 mm->blocks[(srn_block_id_t)index] = block;
453
455
456#if SERENE_DEBUG
457 mm->stats.total_blocks++;
458 mm->stats.total_os_allocations++;
459#endif
460 return (srn_block_id_t)index;
461}
462
464 PANIC_IF_NULL(mm);
465 destroy_chain(mm, id);
466}
467
468#if SERENE_DEBUG
469void srn_mm_print_block_summary(srn_mm_t *mm, srn_block_id_t id) {
470 srn_block_t *block = get_block(mm, id);
471 PANIC_IF_NULL(block);
472
473 printf("Block: %zu@%" PRIXPTR "\n", id, (uintptr_t)block);
474 printf("======================================================\n");
475 printf("Next:\t");
476 if (block->next == nullptr) {
477 printf("X\n");
478 } else {
479 printf("%" PRIXPTR "\n", (uintptr_t)block->next);
480 }
481}
482
483void srn_mm_print_blocks_summary(srn_mm_t *mm) {
484 for (size_t d = 0; d < MAX_NUMBER_OF_BLOCKS; d++) {
485 if (mm->blocks[d] != nullptr) {
486 srn_mm_print_block_summary(mm, d);
487 }
488 }
489}
490#endif
491// 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:463
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:406
static void destroy_chain(srn_mm_t *mm, srn_block_id_t root_id)
Definition default.c:212
static void * alloc_block_internal(srn_mm_t *mm)
Allocate a block worth of memory using the memory provider.
Definition default.c:202
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:190
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:436
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:328
void * srn_mm_reallocate(srn_mm_t *mm, void *ptr, size_t new_size)
Definition default.c:162
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:426
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:169
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:438
size_t srn_mm_get_os_page_size(void)
Retutrns the OS page size.
Definition default.c:313
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:384
static size_t block_remaining(const srn_block_t *block)
Return the number of unused payload bytes left in the block.
Definition default.c:186
static size_t block_capacity(const srn_block_t *block)
Return the size of the payload section of the block.
Definition default.c:178
srn_mm_t * srn_mm_init(const srn_configuration_t *config)
Initialize the memory manager, this function will panic on error.
Definition default.c:332
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:252
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:434
static bool is_block_id_free(const srn_mm_t *mm, uint16_t bit)
Definition default.c:45
#define MM_TRACEPOINT(...)
Definition interface.h:43
#define FALLBACK_PAGE_SIZE
Definition interface.h:45
#define MAX_NUMBER_OF_BLOCKS
array of blocks is enough for us, we can tweak the size as we see fit.
Definition interface.h:53
#define DEFAULT_BLOCK_ALIGNMENT
We strictly use 16 bytes alignment for blocks.
Definition interface.h:56
size_t offset
Offset from the base.
Definition interface.h:88
size_t size
This is the TOTAL size of the block, header + payloud.
Definition interface.h:85
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:80
Every runtime knob, in one place.
srn_mm_config_t mm
This interface is here to abstract over the allocator.
Definition interface.h:72
void *(* allocate)(size_t size, size_t alignment)
Definition interface.h:73
void(* release)(void *p)
Definition interface.h:74
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:110
srn_block_t * blocks[MAX_NUMBER_OF_BLOCKS]
Definition interface.h:129
srn_spinlock_t lock
This spinlock is here to protect the srn_mm_t when allocating/deallocating new blocks.
Definition interface.h:117
srn_block_t * immortal_block
Immortal block is a chain of blocks which will never die.
Definition interface.h:142
size_t block_size
Definition interface.h:125
size_t block_count
Number of live chains.
Definition interface.h:128
srn_spinlock_t chain_locks[MAX_NUMBER_OF_BLOCKS]
One lock per chain, keyed by block id.
Definition interface.h:134
uint64_t block_bitmap[4]
This is a 256bit bitmap we treat it as a whole.
Definition interface.h:124
srn_memory_provider_t * provider
An abstraction over a memory provider like the malloc/free pair.
Definition interface.h:120
srn_spinlock_t immortal_lock
The immortal chain has no block id, so it gets its own chain lock.
Definition interface.h:137
#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