Skip to content

Commit

Permalink
Merge pull request #564 from jasonrandrews/review
Browse files Browse the repository at this point in the history
tested new Learning Path on dynamic memory allocation
  • Loading branch information
pareenaverma authored Nov 6, 2023
2 parents b3dce88 + c907bed commit ed0efef
Show file tree
Hide file tree
Showing 9 changed files with 194 additions and 164 deletions.
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
---
title: Dynamic Memory Allocation
title: Dynamic memory allocation
weight: 2

### FIXED, DO NOT MODIFY
layout: learningpathall
---

## Dynamic vs. Static Allocation
## Dynamic vs. static memory allocation

In this learning path you will learn how to implement dynamic memory allocation.
If you have used C's "heap" (`malloc`, `free`, etc.) before, that is one example
In this Learning Path you will learn how to implement dynamic memory allocation.
If you have used the C programming language "heap" (`malloc`, `free`, etc.) before, that is one example
of dynamic memory allocation.

It allows programs to allocate memory while they are running without knowing
at build time what amount of memory they will need. In constrast to static
memory allocation where the amount is known at build time.
Dynamic memory allocation allows programs to allocate memory while they are running without knowing
at build time how much memory they will need. In contrast, static
memory allocation is used when the amount of memory is known at build time.

The code sample below shows both dynamic and static memory allocation:

```C
#include <stdlib.h>
Expand All @@ -27,10 +29,10 @@ void fn() {
}
```

The example above shows the difference. The size and location of `a` is known
In the example above, the size and location of `a` is known
when the program is built. The size of `b` is also known, but its location is not.

It may even never be allocated, as this pseudocode example shows:
Sometimes, memory may never be allocated, as in the pseudocode example below:

```C
int main(...) {
Expand All @@ -40,37 +42,37 @@ int main(...) {
}
```
If the user passes no arguments to the program, there's no need to allocate space
for `b`. If they do, `malloc` will find space for it.
The arguments passed to the program determine if memory is allocated or not.
## malloc
## The C library malloc function
The C standard library provides a special function
[`malloc`](https://en.cppreference.com/w/c/memory/malloc). `m` for "memory",
`alloc` for "allocate". This can be used to ask for a suitably sized memory
location while the program is running.
`alloc` for "allocate". This is used to ask for a suitably sized memory
location while a program is running.
```C
void *malloc(size_t size);
```

The C library will then look for a chunk of memory with size of at least `size`
The C library looks for a chunk of memory with size of at least `size`
bytes in a large chunk of memory that it has reserved. For instance on Ubuntu
Linux, this will be done by GLIBC.
Linux, this is done by GLIBC.

The example at the top of the page is trivial of course. As it is we could just
statically allocate both integers like this:

```C
void fn() {
int a, b = 0;
}
```

That's ok if this data is never be returned from this function. Or in other
words, if the lifetime of this data is equal to that of the function.
Variables `a` and `b` work fine if they are not needed outside of the function. Or in other
words, if the lifetime of the data is equal to that of the function.

A more complicated example will show you when that is not the case, and the value
lives longer than the function that created it.
A more complex example shows when this is not the case, and the values
live longer than the creating function.

```C
#include <stdlib.h>
Expand All @@ -93,7 +95,7 @@ void add_entry(Entry *entry, int data) {
```
What you see above is a struct `Entry` that defines a singly-linked-list entry.
Singly meaining that you can go forward via `next`, but you cannot go backwards
Singly meaning that you can go forward via `next`, but you cannot go backwards
in the list. There is some data `data`, and each entry points to the next entry,
`next`, assuming there is one (it will be `NULL` for the end of the list).
Expand All @@ -111,55 +113,55 @@ Now you want to add another `Entry` to this list at runtime. So you do not know
ahead of time what it will contain, or if we indeed will add it or not. Where
would you put that entry?

* If it is another global variable, we would have to declare many empty `Entry`s
and hope we never needed more than that amount.
* If it is another global variable, we would have to declare many empty `Entry`
values and hope

{{% notice Other Allocation Techniques%}}
Although in this specific case global variables aren't a good solution, there are
cases where large sets of pre-allocated objects can be beneficial. For example,
it provides a known upper bound of memory usage and makes the timing of each
allocation predictable.

However, we will not be covering these techniques in this learning path. It will
However, these techniques are not covered in this Learning Path. It will
however be useful to think about them after you have completed this learning
path.
{{% /notice %}}

* If it is in a function's stack frame, that stack frame will be reclaimed and
modified by future functions, corrupting the new `Entry`.

So you can see, we must use dynamic memory allocation. Which is why the `add_entry`
So you can see, dynamic memory allocation is required. Which is why the `add_entry`
shown above calls `malloc`. The resulting pointer points to somewhere not in
the program's global data section or in any function's stack space, but in the
heap memory. Where it can live until we `free` it.
heap memory. It will stay in the heap until a call to `free` is made.

## free
## The C library free function

You cannot ask malloc for memory forever. Eventually that space behind the scenes
will run out. So you should give up your dynamic memory once it is not needed,
You cannot ask malloc for memory forever. Eventually the space behind the scenes
will run out. You should give up your dynamic memory once it is not needed,
using [`free`](https://en.cppreference.com/w/c/memory/free).

```C
void free(void *ptr);
```
You call `free` with a pointer previously given to you by `malloc`, and this tells
the heap that we no longer need this memory.
You call `free` with a pointer previously returned by `malloc`, and this tells
the heap that the memory is no longer needed.
{{% notice Undefined Behaviour%}}
You may wonder what happens if you don't pass the exact pointer to `free`, as
`malloc` returned to you. The result varies as this is "undefined behaviour".
{{% notice Undefined Behavior%}}
You may wonder what happens if you don't pass the exact same pointer to `free` as
`malloc` returned. The result varies as this is "undefined behavior".
Which essentially means a large variety of unexpected things can happen.
In practice, many allocators will tolerate this difference or reject it outright
if it's not possible to do something sensbile with the pointer.
if it's not possible to do something sensible with the pointer.
Remember that just because one allocator handles this a certain way, does not
mean all will. Indeed, that same allocator may handle it differently for
Remember, just because one allocator handles this a certain way, does not
mean all allocators will be the same. Indeed, that same allocator may handle it differently for
different allocations within the same program.
{{% /notice %}}
So, you can use `free` to remove an item from your linked list.
You can use `free` to remove an item from your linked list.
```C
void remove_entry(Entry* previous, Entry* entry) {
Expand All @@ -183,5 +185,5 @@ to remove, so that the list skips over it. With `entry` now isolated we call
[A]---------->[C] | [A] [C]
```

That covers the high level how and why of using `malloc` and `free`, next you'll
That covers the high level how and why of using `malloc` and `free`, next you will
see a possible implementation of a dynamic memory allocator.
Original file line number Diff line number Diff line change
@@ -1,63 +1,62 @@
---
title: Designing a Dynamic Memory Allocator
title: Design a dynamic memory allocator
weight: 3

### FIXED, DO NOT MODIFY
layout: learningpathall
---

## High Level Design
## High level design

To begin with, decide which functions your memory allocator will provide. We
have described `malloc` and `free`, there are more provided by the
[C library](https://en.cppreference.com/w/c/memory).

This will assume you just need `malloc` and `free`. Start with those and write
out their behaviours, as the programmer using your allocator will see.
This will assume you just need `malloc` and `free`. The new implementations will
be called `simple_malloc` and `simple_free`. Start with just two functions and write
out their behaviors.

There will be a function, `malloc`. It will:
* Take a size in bytes as a parameter.
* Try to allocate some memory.
* Return a pointer to that memory, NULL pointer otherwise.
The first function is `simple_malloc` and it will:
* Take a size in bytes as a parameter
* Try to allocate the requested memory
* Return a pointer to that memory or return a NULL pointer if the memory cannot be allocated

There will be a function `free`. It will:
* Take a pointer to some previously allocated memory as a parameter.
* Mark that memory as avaiable for future allocations.
The second function is `simple_free` and it will:
* Take a pointer to some previously allocated memory as a parameter
* Mark that memory as available for future allocations

From this you can see that you will need:
* Some large chunk of memory, the "backing storage".
* A way to mark parts of that memory as allocated, or available for allocation.
* A way to mark parts of that memory as allocated, or available for allocation

## Backing Storage
## Backing storage

The memory can come from many sources. It can even change size throughout the
program's execution if you wish. For your allocator you'll keep it as simple
as possible.
program's execution if you wish. For your allocator you can keep it simple.

A single, statically allocated global array of bytes will be your backing
storage. So you can do dynamic allocation of parts of a statically allocated
storage. You can do dynamic allocation of parts of a statically allocated
piece of memory.

```C
#define STORAGE_SIZE 4096
static char storage[STORAGE_SIZE];
```
## Record Keeping
## Record keeping
This backing memory needs to be annotated somehow to record what has been
allocated so far. There are many, many ways to do this. With the biggest choice
here being whether to store these records in the heap itself, our outside of it.
allocated so far. There are many ways to do this. Te biggest choice
is whether to store these records in the heap itself or outside of it.
We will not go into those tradeoffs here, and instead you will put the records
in the heap, as this is relatively simple to do.
The easiest way is to put the records in the heap.
What should be in your records? Think about what question the software will ask
us. Can you give me a pointer to an area of free memory of at least this size?
What should be in the records? Think about the question the caller is asking.
Can you give me a pointer to an area of memory of at least this size?
For this you will need to know:
* Which ranges of the backing storage have been allocated or not.
* How large each of ranges sections is. This includes free areas.
* The ranges of the backing storage that have already been allocated
* The size of each section, both free and allocated
Where a "range" a pointer to a location, a size in bytes and a boolean to say
whether the range is free or allocated. So a range from 0x123 of 345 bytes,
Expand All @@ -67,7 +66,7 @@ that has been allocated would be:
start: 0x123 size: 345 allocated: true
```

For the intial state of a heap of size `N`, you will have one range of
For the initial state of a heap of size `N`, you will have one range of
unallocated memory.

```text
Expand Down Expand Up @@ -102,26 +101,26 @@ Pointer: 0x4 Size: N-4 Allocated: False
range = 0x4 + (N-4) = 1 beyond the end of the heap, so the walk is finished.
```

`free` uses the pointer given to it to find the range it needs to deallocate.
`simple_free` uses the pointer given to it to find the range it needs to deallocate.
Let's say the 4 byte allocation was freed:

```text
Pointer: 0x0 Size: 4 Allocated: False
Pointer: 0x4 Size: N-4 Allocated: False
```

Since `free` gets a pointer directly to the allocation you know exactly which
Since `simple_free` gets a pointer directly to the allocation you know exactly which
range to modify. The only change made is to the boolean which marks it as
allocated or not. The location and size of the range stay the same.

{{% notice Merging Free Ranges%}}
The allocator presented here will not merge free ranges like the 2 above. This
The allocator presented here does not merge free ranges like the 2 above. This
is a deliberate limitation and addressing this is discussed later.
{{% /notice %}}

## Record Storage
## Record storage

You'll keep these records in heap which means using some of the allocated space
You will keep these records in the heap which means using some of the allocated space
for them on top of the allocation itself.

The simplest way to do this is to prepend each allocation with the range
Expand All @@ -135,13 +134,13 @@ ease.
<...and so on until the end of the heap...>
```

Pointers returned by `malloc` are offset to just beyond the range information.
When `free` receives a pointer, it can get to the range information by
Pointers returned by `simple_malloc` are offset to just beyond the range information.
When `simple_free` receives a pointer, it can get to the range information by
subtracting the size of that information from the pointer. Using the example
above:

```text
free(my_ptr);
simple_free(my_ptr);
0x00: [ptr, size, allocated] <-- my_ptr - sizeof(range information)
0x08: <...> <-- my_ptr
Expand All @@ -153,7 +152,7 @@ calculations above must be adjusted. The allocator presented here does not
concern itself with alignment, which is why it can do a simple subtraction.
{{% /notice %}}

## Running Out Of Space
## Running out of space

The final thing an allocator must do is realise it has run out of space. This is
simply achieved by knowing the bounds of the backing storage.
Expand Down
Loading

0 comments on commit ed0efef

Please sign in to comment.