- Published on
Memory Allocation of Fruits
π Memory Allocation of Fruits π
Introduction: The Fruit Basket Problem π
Imagine youβre managing a magical fruit basket. This basket can store different types of fruits (data), like bananas (strings), apples (numbers), and even fruit smoothies (arrays). But thereβs a problem: each new fruit takes up too much space, making access slow and inefficient! ππ¨
To solve this, we need to optimize our basket (memory layout) to avoid waste and ensure fast access. Letβs embark on an adventure to make our fruit basket the best in town! π
Step 1: The NaΓ―ve Approach π
A basic way to store fruits might look like this:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// Different types of fruit data
typedef enum { BOOL, FLOAT, STRING, ARRAY } DataType;
typedef struct {
DataType type;
union {
int b;
float f;
char *s;
float *array;
} value;
} FruitData;
At first glance, this seems fine. We can store bananas (strings), apples (floats), or even a bunch of grapes (arrays). However, every time we add a fruit, it requires a separate memory allocation, leading to inefficiencies. ππ΅
Step 2: Hashing Fruit Names for Faster Lookups π
Instead of storing full fruit names, we can use a hashed fruit ID. This way, instead of storing "Strawberry," we store its numerical equivalent:
#include <stdint.h>
unsigned hash_fruit(const char *fruit_name) {
unsigned hash = 0;
while (*fruit_name) {
hash = hash * 31 + *(fruit_name++);
}
return hash;
}
Now, instead of storing "Banana"
, we store 54321
. This speeds up lookups and reduces memory waste. πβ‘
Step 3: Flattening the Fruit Basket π
Instead of using a hierarchical structure where fruits are nested under categories (citrus, berries, tropical), we flatten everything. This means storing:
fruit.sweetness = 10
fruit.size = 5
fruit.color = 'yellow'
instead of:
fruit = {
sweetness: 10,
size: 5,
color: 'yellow',
}
π Why Flattening Improves Performance?
- Faster Lookups β No need to traverse a tree structure; properties are accessed instantly.
- Less Complexity β No deep nesting, making data handling easier.
- Database Efficiency β NoSQL databases prefer flat structures for better indexing.
- Reduced Memory Overhead β Nested structures require extra memory for object references.
Step 4: Optimizing with Structure of Arrays (SoA) π
Instead of storing fruit data as an Array of Structures (AoS), we switch to a Structure of Arrays (SoA) for better cache efficiency.
typedef enum { BOOL, FLOAT, STRING, OBJECT, ARRAY } DataType;
typedef struct {
unsigned *keys;
DataType *types;
void **values;
size_t count;
size_t capacity;
} FruitBasket;
FruitBasket* createFruitBasket(size_t capacity) {
FruitBasket *basket = malloc(sizeof(FruitBasket));
if (!basket) return NULL;
void *data = malloc(capacity * (sizeof(unsigned) + sizeof(DataType) + sizeof(void *)));
if (!data) {
free(basket);
return NULL;
}
basket->keys = (unsigned *)data;
basket->types = (DataType *)(basket->keys + capacity);
basket->values = (void **)(basket->types + capacity);
basket->count = 0;
basket->capacity = capacity;
return basket;
}
void freeFruitBasket(FruitBasket *basket) {
if (basket) {
free(basket->keys); // Free all arrays at once
free(basket);
}
}
This stores keys, types, and values in separate arrays for better memory locality, making access much faster! ποΈπ¨
π When is SoA Better?
- When accessing all attributes of a single object (e.g., one fruit at a time), AoS is better.
- When accessing one attribute across multiple objects (e.g., colors of all fruits), SoA is better.
| AoS (Original) | SoA (Optimized) | | -------------- | --------------- | ------ | -------- | ------- | -------- | | key | type | value
| key_0 | key_1 | key_2
| | key | type | value
| type_0 | type_1 | type_2
| | key | type | value
| value_0 | value_1 | value_2
|
Step 5: Efficient Memory Allocation with Pre-Allocated Buffers π
Instead of calling malloc()
for every fruit, we pre-allocate a memory buffer:
typedef struct {
char *buffer;
size_t capacity;
size_t used;
} FruitMemory;
void *allocate_memory(FruitMemory *fm, size_t size) {
if (fm->used + size > fm->capacity) return NULL;
void *ptr = fm->buffer + fm->used;
fm->used += size;
return ptr;
}
π Why is This Better?
- Reduces malloc overhead β Calling
malloc()
repeatedly can cause fragmentation and slow performance. - Better Cache Utilization β Pre-allocating ensures that memory is contiguous, improving cache hits.
- Simplified Deallocation β Instead of freeing multiple small allocations, we free the entire buffer at once.
Example usage:
FruitMemory fm = {malloc(1024), 1024, 0};
char *buffer = allocate_memory(&fm, capacity * (sizeof(char*) + sizeof(int) + sizeof(float)));
char **names = (char **)buffer;
int *types = (int *)(names + capacity);
float *prices = (float *)(types + capacity);
Now, adding a new fruit simply takes a slice from the buffer instead of requesting memory dynamically. πβ¨
π The Fruit Basket (Memory Buffer)
Imagine you have a basket π§Ί with a fixed size (e.g., 10 slots).
This basket represents our memory buffer.
We will store fruit names (headers) on the left side and actual fruits (values) on the right side.
ππ Adding Fruits and Labels
- We first place labels (fruit names) on the left side.
- We then place the actual fruits on the right side.
- The free space in the middle shrinks as we keep adding.
π Example Scenario (Adding Fruits)
Header (Names) β | π Apple | π Banana | π© Free Space π© | π Orange | π Watermelon | β Value (Fruits) |
---|
- Apple label is stored on the left.
- The actual Apple fruit is stored on the right.
- Banana label is added next to Apple on the left.
- The actual Banana fruit is stored on the right, next to Apple.
π If Space Runs Out?
If we keep adding more fruit without enough space, the two sides will collide in the middle, meaning:
- The basket is full π§Ί.
- We must get a bigger basket (reallocate memory).
- The fruits are rearranged into the new larger basket.
π Code Representation
Hereβs how this works in C code:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// The basket (memory buffer)
typedef struct {
char *basket; // Single buffer for fruit names & fruits
unsigned capacity; // Total space
unsigned label_size; // Space used by fruit names
unsigned fruit_size; // Space used by actual fruits
} FruitBasket;
// Initialize the basket
FruitBasket *init_fruit_basket(unsigned capacity) {
FruitBasket *fb = malloc(sizeof(FruitBasket));
if (!fb) return NULL;
fb->basket = malloc(capacity);
if (!fb->basket) {
free(fb);
return NULL;
}
fb->capacity = capacity;
fb->label_size = 0;
fb->fruit_size = 0;
return fb;
}
// Store a fruit name (growing left to right)
void *store_label(FruitBasket *fb, const char *name) {
unsigned size = strlen(name) + 1;
if (fb->label_size + fb->fruit_size + size > fb->capacity)
return NULL; // No space left
char *res = fb->basket + fb->label_size;
strcpy(res, name);
fb->label_size += size;
return res;
}
// Store an actual fruit (growing right to left)
void *store_fruit(FruitBasket *fb, const char *fruit) {
unsigned size = strlen(fruit) + 1;
if (fb->label_size + fb->fruit_size + size > fb->capacity)
return NULL; // No space left
fb->fruit_size += size;
char *res = fb->basket + fb->capacity - fb->fruit_size;
strcpy(res, fruit);
return res;
}
// Print basket contents
void print_basket(FruitBasket *fb) {
printf("Total Capacity: %u\n", fb->capacity);
printf("Label Space Used: %u\n", fb->label_size);
printf("Fruit Space Used: %u\n", fb->fruit_size);
printf("Free Space: %u\n", fb->capacity - (fb->label_size + fb->fruit_size));
}
// Free memory
void free_basket(FruitBasket *fb) {
if (fb) {
free(fb->basket);
free(fb);
}
}
// Example Usage
int main() {
FruitBasket *fb = init_fruit_basket(100); // 100-byte basket
char *apple_label = store_label(fb, "Apple");
char *apple_fruit = store_fruit(fb, "π");
char *banana_label = store_label(fb, "Banana");
char *banana_fruit = store_fruit(fb, "π");
print_basket(fb);
printf("Fruit Label 1: %s -> %s\n", apple_label, apple_fruit);
printf("Fruit Label 2: %s -> %s\n", banana_label, banana_fruit);
free_basket(fb);
return 0;
}
π Output Example
Total Capacity: 100
Label Space Used: 13
Fruit Space Used: 10
Free Space: 77
Fruit Label 1: Apple -> π
Fruit Label 2: Banana -> π
π Summary
- The fruit basket (buffer) stores both labels and fruits.
- Labels grow left to right π·οΈ.
- Fruits grow right to left π.
- If they meet in the middle, we need a bigger basket π§Ί.
This is an efficient memory management technique that minimizes fragmentation! π
Conclusion: A Super-Efficient Fruit Basket! π
By applying these optimizations, weβve transformed our messy fruit basket into a cache-friendly, memory-efficient system:
β
Hashing reduces lookup times.
β
Flattening simplifies data access.
β
Structure of Arrays enhances performance.
β
Pre-allocated Buffers minimize allocation overhead.
So, next time youβre designing data structures, think of memory allocation as a fruit basketβand make sure your bananas, apples, and grapes are stored efficiently! πππ