Всем привет! 
Продолжим тему с аллокатором, который я реализовала для хранения различных данных вокселей. Как я уже рассказывала в предыдущем посте, я использую достаточно простой подход - обычный пуллинг памяти.
Как же устроен мой пул? 
1️⃣ Я выделяю заранее буфер на много-много указателей блоков. Да, я храню не сами блоки, я храню только указатели (8 байт). Сколько таких блоков может быть в пуле - подобрано эвристически в зависимости от моих задач. Размер этого буфера не меняется.
2️⃣ Я выделяю также и общий список Next Free. Этот список для каждого элемента хранит индекс следующего свободного. Получается односвязный список, по которому я могу найти за O(1) следующий свободный элемент. Скорость мне очень важна.
3️⃣ Я выделяю Thread Local Storage. Это необходимо, чтобы было как можно меньше пересечений между потоками и кэш-миссов из-за инвалидации кэша + меньше блокировок. В TLS хранится следующий свободный индекс для каждого потока.
Как же я ищу блок в пуле? 
Давайте представим, что мы находимся в каком потоке.
1️⃣ Я смотрю в Thread Local Storage по индексу этого потока. Вдруг там есть свободный индекс, которые мы можем использовать.
2️⃣ Если не нашла, то я пытаюсь забрать свободные индексы, которые пока не принадлежат ни одному потоку. Дело в том, что я не разбиваю сразу все индексы из пула по всем потокам, я их "отдаю" пачками, по 16 штук при необходимости.
3️⃣ Получив индекс так или иначе, я обновляю связаный список Next Free и TLS. Это просто установка значения, что занимает снова O(1).
4️⃣ По полученному индексу я получаю указатель блока из пула, если он был раннее аллоцирован. Если же нет, то аллоцирую новый блок по указанному back-аллокатору (чаще всего, это Persistent).
Пример как можно найти доступный индекс:
do
{
do
{
// читаем из TLS текущего потока
idx = Volatile.Read(ref TLS[threadIndex]);
}while (idx == -3);
if(idx < 0)
// в TLS нашего потока больше нет ничего
}while (Interlocked.CompareExchange(ref TLS[threadIndex], -3, idx) != idx);
// записали следующий свободный в наш TLS
Interlocked.Exchange(ref TLS[threadIndex], nextPtrs[idx]);
Пример как можно "отдать" еще 16 индексов потоку:
Interlocked.Exchange(ref poolData->FirstFreeTLS[threadIndex], -2); // помечаем, что мы будем еще выделять
if (allocatedCount < capacity)
{
idx = Interlocked.Add(ref allocatedCount, 16) - 16; // пытаемся отдать все 16 индексов
if (idx < capacity - 1)
{
var count = math.min(16, capacity - idx);
for (var i = 1; i < count; ++i)
{
// сразу их записываем в список свободных
nextPtrs[idx + i] = idx + i + 1;
}
nextPtrs[idx + count - 1] = -1;
nextPtrs[idx] = -1; // текущий мы отдаем, поэтому он не свободен
// и в TLS тоже записываем следующий свободный
Interlocked.Exchange(ref TLS[threadIndex], idx + 1);
return idx;
}
if (idx == capacity - 1)
{
// это был последний доступный в пуле, значит помечаем как "больше элементов нет"
Interlocked.Exchange(ref TLS[threadIndex], -1);
return idx;
}
}
// ничего не нашли, следующих свободных нет
Interlocked.Exchange(ref TLS[threadIndex], -1);
А что если мы использовали все, что было в пуле, и что места под блоки? Тогда для потока я не смогу найти еще 16 индексов.
Получается, что мы израсходовали все, что у нас было. Но, вдруг, у других потоков еще есть свободные индексы?
Попробуем украсть их!
Алгоритм кражи тоже простой:
1️⃣ Пробегаемся по всем TLS всех потоков, кроме нашего текущего.
2️⃣ Смотрим, есть ли у них что-то, что можно украсть.
3️⃣ Если есть, то крадем. В TLS потока, у которого крадем, помечаем, что мы забрали индекс.
Пример кражи:
for(var threadIndex = 0; threadIndex != currentIndex; threadIndex < threadCount; threadIndex++)
{
do
{
do
{
idx = Volatile.Read(ref TLS[threadIndex]);
}while(idx == -3);
if(idx < 0)
break;
}while (Interlocked.CompareExchange(ref TLS[threadIndex], -3, idx) != idx);
}
Спасибо за внимание! 
#generation #allocators