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