Taro Logo

Design Memory Allocator

Medium
Meta logo
Meta
5 views
Topics:
Arrays

You are given an integer n representing the size of a 0-indexed memory array. All memory units are initially free.

You have a memory allocator with the following functionalities:

  1. Allocate a block of size consecutive free memory units and assign it the id mID.
  2. Free all memory units with the given id mID.

Note that:

  • Multiple blocks can be allocated to the same mID.
  • You should free all the memory units with mID, even if they were allocated in different blocks.

Implement the Allocator class:

  • Allocator(int n) Initializes an Allocator object with a memory array of size n.
  • int allocate(int size, int mID) Find the leftmost block of size consecutive free memory units and allocate it with the id mID. Return the block's first index. If such a block does not exist, return -1.
  • int freeMemory(int mID) Free all memory units with the id mID. Return the number of memory units you have freed.

For example:

Allocator loc = new Allocator(10); // Initialize a memory array of size 10. All memory units are initially free.
loc.allocate(1, 1); // The leftmost block's first index is 0. The memory array becomes [1,_,_,_,_,_,_,_,_,_]. We return 0.
loc.allocate(1, 2); // The leftmost block's first index is 1. The memory array becomes [1,2,_,_,_,_,_,_,_,_]. We return 1.
loc.allocate(1, 3); // The leftmost block's first index is 2. The memory array becomes [1,2,3,_,_,_,_,_,_,_]. We return 2.
loc.freeMemory(2); // Free all memory units with mID 2. The memory array becomes [1,_, 3,_,_,_,_,_,_,_]. We return 1 since there is only 1 unit with mID 2.
loc.allocate(3, 4); // The leftmost block's first index is 3. The memory array becomes [1,_,3,4,4,4,_,_,_,_]. We return 3.

How would you implement the Allocator class to efficiently manage memory allocation and deallocation?

Solution


Clarifying Questions

When you get asked this question in a real-life environment, it will often be ambiguous (especially at FAANG). Make sure to ask these questions in that case:

  1. What are the maximum values for n (memory size), size (allocation size), and id? Also, can size or n be zero?
  2. If allocate fails to find a suitable block, what should happen if I call free with the same id I attempted to allocate with?
  3. Does the memory need to be persistent between allocate and free calls, or can I assume that the memory is reset each time allocate or free is called?
  4. Are overlapping allocations with different IDs allowed? If so, how should I handle freeing in cases where overlapping allocations exist?
  5. If `free(id)` is called with an id that has not been previously allocated, what value should I return?

Brute Force Solution

Approach

The brute force strategy for memory allocation involves checking every possible place to put a new block of data. Think of it like trying to fit a new box into a shelf by trying every available space, one by one. We will keep trying until we find an available space that works.

Here's how the algorithm would work step-by-step:

  1. When someone asks to allocate memory, start at the very beginning of the available memory.
  2. See if there's enough space starting from that first location.
  3. If there's not enough space, move to the next available spot.
  4. Keep checking each spot, one after another, until you find a spot that's big enough.
  5. Once you find a big enough spot, mark that part of memory as now being used.
  6. If you reach the end of memory without finding a suitable space, tell the person requesting memory that there isn't any left.
  7. When someone frees memory, mark that part of memory as available again.
  8. Whenever a new request for memory comes in, repeat the whole process from the beginning.

Code Implementation

class MemoryAllocator:

def __init__(self, memory_size):
        self.memory_size = memory_size
        self.memory = [0] * memory_size # 0 means free, 1 means allocated

def allocate(self, size):
        # Iterate through memory to find a suitable block
        for start_index in range(self.memory_size - size + 1):

            is_available = True
            for index in range(start_index, start_index + size):
                if self.memory[index] == 1:
                    is_available = False
                    break

            if is_available:
                # Mark the block as allocated, since it's available

                for index in range(start_index, start_index + size):
                    self.memory[index] = 1

                return start_index

        return -1 # Indicate allocation failure

def free(self, start_index, size):
        # Ensure the start index and size are within bounds
        if 0 <= start_index < self.memory_size and start_index + size <= self.memory_size:

            # Mark the specified memory region as free
            for index in range(start_index, start_index + size):
                self.memory[index] = 0

Big(O) Analysis

Time Complexity
O(n)In the allocation function, the algorithm iterates through the entire memory space of size n to find a suitable block. Even though it stops once it finds a free block of the required size, in the worst case (when the memory is full or the required block is only available at the very end), it will traverse the entire memory space once. Therefore, the allocation function has a time complexity of O(n). The deallocation function simply marks a block as free, which takes constant time, O(1). Since the allocation dominates, the overall time complexity for a single allocation/deallocation operation remains O(n).
Space Complexity
O(1)The provided memory allocation strategy operates directly on the existing memory space without using significant auxiliary data structures. It only requires a few variables, such as pointers or indices, to keep track of the current location during the search for available space. The number of these variables remains constant regardless of the total memory size, denoted as N. Therefore, the space complexity is constant, O(1).

Optimal Solution

Approach

The best way to manage memory efficiently is to keep track of what parts are free and what parts are used. We'll use a way to quickly find a free chunk big enough when someone asks for memory, and then update our records to show that chunk is now used.

Here's how the algorithm would work step-by-step:

  1. First, divide the total memory into blocks, and keep track of whether each block is available or in use.
  2. When someone asks for a certain amount of memory, scan through the available blocks to find the first one that's big enough.
  3. If a suitable block is found, mark it as 'in use'. If the block is much larger than what was requested, split it into two: a used part of the requested size, and a smaller unused part.
  4. When memory is no longer needed, mark its blocks as available again.
  5. To make things even faster, when we find a free block, we can combine it with any free blocks right next to it, creating a bigger free block. This avoids small fragmented spaces.

Code Implementation

class MemoryAllocator:
    def __init__(self, memory_size):
        self.memory_size = memory_size
        self.memory = [False] * memory_size
        self.free_blocks = [(0, memory_size)]

    def allocate(self, size):
        for i, (start, block_size) in enumerate(self.free_blocks):
            if block_size >= size:
                # Found a block large enough
                allocated_start = start

                if block_size > size:
                    # Split the block if it's much larger than requested
                    self.free_blocks[i] = (start + size, block_size - size)
                else:
                    del self.free_blocks[i]

                for j in range(allocated_start, allocated_start + size):
                    self.memory[j] = True

                return allocated_start

        return None

    def deallocate(self, start, size):
        for i in range(start, start + size):
            self.memory[i] = False

        # Inserting the freed block
        self.free_blocks.append((start, size))
        self.free_blocks.sort()

        self.merge_free_blocks()

    def merge_free_blocks(self):
        # Iterate through the free blocks and merge contiguous blocks
        i = 0
        while i < len(self.free_blocks) - 1:
            start1, size1 = self.free_blocks[i]
            start2, size2 = self.free_blocks[i+1]

            if start1 + size1 == start2:
                # Merge adjacent free blocks
                self.free_blocks[i] = (start1, size1 + size2)
                del self.free_blocks[i+1]
            else:
                i += 1

        #Merging is complete, so we can exit the function
        return

Big(O) Analysis

Time Complexity
O(n)The allocation process involves scanning the list of memory blocks to find a free block that satisfies the memory request. In the worst-case scenario, where no suitable block is found until the end of the list, or when coalescing free blocks, the entire list needs to be traversed. Therefore, the time complexity for allocation and coalescing is directly proportional to the number of blocks, n, leading to O(n). Deallocation also takes O(n) as the list might need to be scanned to coalesce adjacent free blocks.
Space Complexity
O(N)The algorithm divides the total memory into blocks and keeps track of whether each block is available or in use. This requires a data structure, such as an array or linked list, to store the status of each block. The size of this data structure is directly proportional to the total memory size, N, where N is the number of memory blocks. Therefore, the auxiliary space required is O(N) due to the need to track the availability of each memory block. No significant additional space is used beyond tracking block availability.

Edge Cases

CaseHow to Handle
Memory size n is 0Allocate should always return -1 and Free should return 0 since no memory is available.
Allocate size is 0Allocate should return a valid index if any is available, and assign the id to a block of size 0; otherwise return -1.
Allocate size is greater than nAllocate should always return -1 because requested size exceeds total memory.
Allocate id already existsThe problem statement does not specify behavior; either reject the allocation, free the old allocation and proceed, or overwrite the old allocation directly, each should be clearly defined.
Free id does not existFree should return 0 indicating that no blocks were freed.
Multiple free blocks exist for a single ID (from potential multiple allocations).Free should free *all* blocks allocated with the specified id and return the total number of freed blocks.
Memory is completely full and allocate is calledAllocate should return -1 as there is no space available to allocate.
Large n leading to potential memory constraints for internal data structures.Choose efficient data structures like bit arrays or linked lists to represent memory allocation to avoid excessive memory usage.