MemoryAllocator проще всего представить как один большой неразрывный массив байт.
Чтобы положить туда данные - нужно всего лишь знать по какому индексу это делать.
Для этого аллокатор разбивается на блоки. В пустом аллокаторе блок всего один, он занимает всю область памяти от начала и до конца.
Блок - это структура, у которой есть часть заголовка (с указателями на следующий/предыдущий блоки, состоянием "свободен"/"занят" и размером блока) и следом сами данные.
[block_size][state][prev][next][user_data]
Когда мы просим аллокатор дать нам память определенного размера, нам нужно найти свободный блок памяти. Тут мы просто переходим от первого блока до последнего и ищем блок подходящего размера. Если блок не нашли - добавляем новый. А возвращаем мы не unsafe-указатель, а собственный указатель, в котором записан тот самый индекс в нашем массиве байт.
Когда мы просим освободить память, мы выставляем блоку состояние "свободен" и мерджим его с соседними свободными блоками, если такие есть.
Еще есть дополнительные всякие штуки, что перебираем мы не с самого начала, а с первого пустого, можно вообще завести отдельный список пустых блоков и выбирать среди них, можно еще сортировать их по размеру, а потом искать бинарным поиском. Еще есть зоны, которые содержат блоки, это вообще отдельная история.
Таким образом мы получаем следующие бенефиты:
1. Большой кусок памяти, который мы можем скопировать/передать по сети/уничтожить очень быстро;
2. Выдаваемые указатели можно так же передавать по сети, т.к. они будут валидны на любом клиенте, если тот имеет такой же аллокатор;
3. Нам не нужно заботиться об уничтожении данных, достаточно просто убить весь аллокатор.
Реализацию можно посмотреть тут:https://github.com/chromealex/csharp-memory-allocator