6

How would one go about creating a custom MemoryManager to manage a given, contiguous chunk of memory without the aid of other memory managers (such as Malloc/New) in C++?

Here's some more context:

   MemManager::MemManager(void* memory, unsigned char totalsize)
   {
       Memory = memory;
       MemSize = totalsize;
   }

I need to be able to allocate and free up blocks of this contiguous memory using a MemManager. The constructor is given the total size of the chunk in bytes.

An Allocate function should take in the amount of memory required in bytes and return a pointer to the start of that block of memory. If no memory is remaining, a NULL pointer is returned.

A Deallocate function should take in the pointer to the block of memory that must be freed and give it back to the MemManager for future use.

Note the following constraints:

-Aside from the chunk of memory given to it, the MemManager cannot use ANY dynamic memory

-As originally specified, the MemManager CANNOT use other memory managers to perform its functions, including new/malloc and delete/free

I have received this question on several job interviews already, but even hours of researching online did not help me and I have failed every time. I have found similar implementations, but they have all either used malloc/new or were general-purpose and requested memory from the OS, which I am not allowed to do.

Note that I am comfortable using malloc/new and free/delete and have little trouble working with them.

I have tried implementations that utilize node objects in a LinkedList fashion that point to the block of memory allocated and state how many bytes were used. However, with those implementations I was always forced to create new nodes onto the stack and insert them into the list, but as soon as they went out of scope the entire program broke since the addresses and memory sizes were lost.

If anyone has some sort of idea of how to implement something like this, I would greatly appreciate it. Thanks in advance!

EDIT: I forgot to directly specify this in my original post, but the objects allocated with this MemManager can be different sizes.

EDIT 2: I ended up using homogenous memory chunks, which was actually very simple to implement thanks to the information provided by the answers below. The exact rules regarding the implementation itself were not specified, so I separated each block into 8 bytes. If the user requested more than 8 bytes, I would be unable to give it, but if the user requested fewer than 8 bytes (but > 0) then I would give extra memory. If the amount of memory passed in was not divisible by 8 then there would be wasted memory at the end, which I suppose is much better than using more memory than you're given.

  • The trick is to store metadata slightly below the allocated address. Too tired to write full explanation right now. – Jason Oct 16 '14 at 05:01

2 Answers2

1

I have tried implementations that utilize node objects in a LinkedList fashion that point to the block of memory allocated and state how many bytes were used. However, with those implementations I was always forced to create new nodes onto the stack and insert them into the list, but as soon as they went out of scope the entire program broke since the addresses and memory sizes were lost.

You're on the right track. You can embed the LinkedList node in the block of memory you're given with reinterpret_cast<>. Since you're allowed to store variables in the memory manager as long as you don't dynamically allocate memory, you can track the head of the list with a member variable. You might need to pay special attention to object size (Are all objects the same size? Is the object size greater than the size of your linked list node?)

Assuming the answers to the previous questions to be true, you can then process the block of memory and split it off into smaller, object sized chunks using a helper linked list that tracks free nodes. Your free node struct will be something like

struct FreeListNode
{
    FreeListNode* Next;
};

When allocating, all you do is remove the head node from the free list and return it. Deallocating is just inserting the freed block of memory into the free list. Splitting the block of memory up is just a loop:

// static_cast only needed if constructor takes a void pointer; can't perform pointer arithmetic on void*
char* memoryEnd = static_cast<char*>(memory) + totalSize; 
for (char* blockStart = block; blockStart < memoryEnd; blockStart += objectSize)
{
    FreeListNode* freeNode = reinterpret_cast<FreeListNode*>(blockStart);
    freeNode->Next = freeListHead;
    freeListHead = freeNode;
}

As you mentioned the Allocate function takes in the object size, the above will need to be modified to store metadata. You can do this by including the size of the free block in the free list node data. This removes the need to split up the initial block, but introduces complexity in Allocate() and Deallocate(). You'll also need to worry about memory fragmentation, because if you don't have a free block with enough memory to store the requested amount, there's nothing that you can do other than to fail the allocation. A couple of Allocate() algorithms might be:

1) Just return the first available block large enough to hold the request, updating the free block as necessary. This is O(n) in terms of searching the free list, but might not need to search a lot of free blocks and could lead to fragmentation problems down the road.

2) Search the free list for the block that has the smallest amount free in order to hold the memory. This is still O(n) in terms of searching the free list because you have to look at every node to find the least wasteful one, but can help delay fragmentation problems.

Either way, with variable size, you have to store metadata for allocations somewhere as well. If you can't dynamically allocate at all, the best place is before or after the user requested block; you can add features to detect buffer overflows/underflows during Deallocate() if you want to add padding that is initialized to a known value and check the padding for a difference. You can also add a compact step as mentioned in another answer if you want to handle that.

One final note: you'll have to be careful when adding metadata to the FreeListNode helper struct, as the smallest free block size allowed is sizeof(FreeListNode). This is because you are storing the metadata in the free memory block itself. The more metadata you find yourself needing to store for your internal purposes, the more wasteful your memory manager will be.

masrtis
  • 1,282
  • 17
  • 32
  • Thank you so much! I had no idea about using reinterpret_cast in that way. Does static_cast also work well for using pointer arithmetic with bytes? I have been using it and experienced no problems. In any case, I will give this a go tomorrow and see what I can do. Thanks again! –  Oct 16 '14 at 05:06
  • 1
    @Kimimaru You can use static cast to cast void* to X* where X is any type, and back again See item 10 from [here](http://en.cppreference.com/w/cpp/language/static_cast). For the reinterpret_cast in the loop body, you have to use reinterpret_cast and you can only safely do this if you are casting from char*; see the section on type aliasing in this [reinterpret_cast documentation](http://en.cppreference.com/w/cpp/language/reinterpret_cast). Ideally your MemManager interface would take a char* directly, eliminating the first cast in my code and making the second safe. – masrtis Oct 16 '14 at 05:29
  • I'm implementing it now and I'm having trouble with a few things. First, your description of deallocation implies that I must remove the head from the free list when allocating; is this true? Second, since I cannot rely on statically-sized memory blocks if the amount of memory passed into the Allocate function is a variable size, what would your recommendation be for tracking allocations and how much memory they take up? –  Oct 16 '14 at 19:32
1

When you manage memory, you generally want to use the memory you manage to store any metadata you need. If you look at any of the implementations of malloc (ptmalloc, phkmalloc, tcmalloc, etc...), you'll see that this is how they're generally implemented (neglecting any static data of course). The algorithms and structures are very different, for different reasons, but I'll try to give a little insight into what goes into generic memory management.

Managing homogeneous chunks of memory is different than managing non-homogeneous chunks, and it can be a lot simpler. An example...

MemoryManager::MemoryManager() {

  this->map = std::bitset<count>();
  this->mem = malloc(size * count);

  for (int i = 0; i < count; i++)
    this->map.set(i);
}

Allocating is a matter of finding the next bit in the std::bitset (compiler might optimize), marking the chunk as allocated and returning it. De-allocation just requires calculating the index, and marking as unallocated. A free list is another way (what's described here), but it's a little less memory efficient, and might not use CPU cache well.

A free list can be the basis for managing non-homogenous chunks of memory though. With this, you need to store the size of the chunks, in addition to the next pointer in the chunk of memory. The size lets you split larger chunks into smaller chunks. This generally leads to fragmentation though, since merging chunks is non-trivial. This is why most data structures keep lists of same sized chunks, and try to map requests as closely as possible.

Community
  • 1
  • 1
Jason
  • 3,777
  • 14
  • 27
  • Thanks for your response! For homogenous chunks, what happens if the user requests more memory than a single chunk provides? –  Oct 16 '14 at 19:46
  • 2
    For the homogeneous chunks, the allocate method probably shouldn't allow the user to specify allocate size. For non-homogeneous chunks, it gets interesting :). If you're using a free list, you *can* try to merge contiguous chunks, but this is extremely complex. This is one of the reasons fragmentation is bad. Normally, a memory manager would directly allocate from the system using `mmap` or an equivalent system call. Without allocating additional memory, you'd need to return null though. – Jason Oct 16 '14 at 19:59