- Эффективные деревья квадрантов
Ладно, я попробую. Сначала тизер, чтобы показать результаты того, что я предложу с участием 20 000 агентов (просто то, что я очень быстро придумал для этого конкретного вопроса):
![введите здесь описание изображения](https://i.stack.imgur.com/6cQeQ.gif)
GIF имеет чрезвычайно низкую частоту кадров и значительно более низкое разрешение, чтобы соответствовать максимальному размеру 2 МБ для этого сайта. Вот видео, если вы хотите увидеть это на полной скорости: https://streamable.com/3pgmn .
И GIF со 100 КБ, хотя мне пришлось так много возиться с ним, и мне пришлось отключить линии quadtree (похоже, не хотел так сильно сжимать с ними), а также изменить способ, которым агенты выглядели, чтобы получить его помещается в 2 мегабайта (хотелось бы, чтобы создание GIF было таким же простым, как кодирование дерева квадрантов):
![введите здесь описание изображения](https://i.stack.imgur.com/3qBeV.gif)
Симуляция с 20 000 агентов занимает ~3 мегабайта оперативной памяти. Я также могу легко обрабатывать 100 000 агентов меньшего размера, не жертвуя частотой кадров, хотя это приводит к некоторому беспорядку на экране до такой степени, что вы едва можете видеть, что происходит, как на GIF выше. Все это выполняется в одном потоке на моем i7, и я трачу почти половину времени, согласно VTune, просто рисуя эти вещи на экране (просто используя некоторые базовые скалярные инструкции для построения объектов по одному пикселю за раз в ЦП).
А вот видео со 100 000 агентов, хотя трудно понять, что происходит. Это довольно большое видео, и я не смог найти приличного способа сжать его, чтобы все видео не превратилось в кашу (возможно, сначала потребуется загрузить или кэшировать его, чтобы увидеть, как оно транслируется с разумной частотой кадров). С 100 тыс. агентов симуляция занимает около 4,5 мегабайт ОЗУ, и использование памяти очень стабильно после запуска симуляции в течение примерно 5 секунд (перестает увеличиваться или уменьшаться, поскольку перестает выделяться куча). То же самое в замедленной съемке.
Эффективное дерево квадрантов для обнаружения столкновений
Итак, на самом деле деревья квадрантов не являются моей любимой структурой данных для этой цели. Я предпочитаю иерархическую сетку, например грубую сетку для мира, более мелкую сетку для региона и еще более тонкую сетку для субрегиона (3 фиксированных уровня плотных сеток и без участия деревьев), со строками. на основе оптимизации, так что строка, в которой нет сущностей, будет освобождена и превращена в нулевой указатель, а также полностью пустые области или подобласти превращены в нули. Хотя эта простая реализация дерева квадрантов, работающего в одном потоке, может обрабатывать 100 000 агентов на моем i7 со скоростью 60+ кадров в секунду, я реализовал сети, которые могут обрабатывать пару миллионов агентов, отскакивающих друг от друга в каждом кадре, на более старом оборудовании (i3). Также мне всегда нравилось, как с помощью сеток очень легко предсказать, сколько памяти им потребуется, поскольку они не подразделяют ячейки. Но я попытаюсь рассказать, как реализовать достаточно эффективное дерево квадрантов.
Обратите внимание, что я не буду вдаваться в полную теорию структуры данных. Я предполагаю, что вы уже это знаете и заинтересованы в повышении производительности. Я также просто расскажу о своем личном способе решения этой проблемы, который, кажется, превосходит большинство решений, которые я нахожу в Интернете для своих случаев, но есть много достойных способов, и эти решения адаптированы к моим случаям использования (очень большие входные данные со всем, что движется в каждом кадре для визуальных эффектов в фильмах и на телевидении). Другие люди, вероятно, оптимизируют для других вариантов использования. Когда речь идет, в частности, о структурах пространственного индексирования, я действительно думаю, что эффективность решения говорит вам больше о разработчике, чем о структуре данных. Кроме того, те же стратегии, которые я предлагаю для ускорения работы, также применимы в трехмерном пространстве с октодеревьями.
Представление узла
Итак, прежде всего, давайте рассмотрим представление узла:
// Represents a node in the quadtree.
struct QuadNode
{
// Points to the first child if this node is a branch or the first
// element if this node is a leaf.
int32_t first_child;
// Stores the number of elements in the leaf or -1 if it this node is
// not a leaf.
int32_t count;
};
Всего 8 байт, и это очень важно, так как это ключевая часть скорости. На самом деле я использую меньший (6 байт на узел), но я оставлю это читателю в качестве упражнения.
Вы, вероятно, можете обойтись без count
. Я включаю это для патологических случаев, чтобы избежать линейного обхода элементов и подсчета их каждый раз, когда конечный узел может разделиться. В большинстве случаев узел не должен хранить столько элементов. Тем не менее, я работаю с визуальными эффектами, и патологические случаи не обязательно редки. Вы можете столкнуться с художниками, создающими контент с кучей совпадающих точек, массивными полигонами, охватывающими всю сцену, и т. д., поэтому в итоге я сохраняю файл count
.
Где AABB?
Таким образом, одна из первых вещей, которые могут возникнуть у людей, — это расположение ограничивающих рамок (прямоугольников) для узлов. Я их не храню. Я вычисляю их на лету. Я немного удивлен, что большинство людей не делают этого в коде, который я видел. Для меня они хранятся только с древовидной структурой (в основном только один AABB для корня).
Может показаться, что вычислять их на лету будет дороже, но сокращение использования памяти узлом может пропорционально уменьшить промахи кэша при обходе дерева, и эти сокращения промахов кэша, как правило, более значительны, чем необходимость сделать пару битовых сдвигов и некоторые добавления/вычитания во время обхода. Обход выглядит так:
static QuadNodeList find_leaves(const Quadtree& tree, const QuadNodeData& root, const int rect[4])
{
QuadNodeList leaves, to_process;
to_process.push_back(root);
while (to_process.size() > 0)
{
const QuadNodeData nd = to_process.pop_back();
// If this node is a leaf, insert it to the list.
if (tree.nodes[nd.index].count != -1)
leaves.push_back(nd);
else
{
// Otherwise push the children that intersect the rectangle.
const int mx = nd.crect[0], my = nd.crect[1];
const int hx = nd.crect[2] >> 1, hy = nd.crect[3] >> 1;
const int fc = tree.nodes[nd.index].first_child;
const int l = mx-hx, t = my-hx, r = mx+hx, b = my+hy;
if (rect[1] <= my)
{
if (rect[0] <= mx)
to_process.push_back(child_data(l,t, hx, hy, fc+0, nd.depth+1));
if (rect[2] > mx)
to_process.push_back(child_data(r,t, hx, hy, fc+1, nd.depth+1));
}
if (rect[3] > my)
{
if (rect[0] <= mx)
to_process.push_back(child_data(l,b, hx, hy, fc+2, nd.depth+1));
if (rect[2] > mx)
to_process.push_back(child_data(r,b, hx, hy, fc+3, nd.depth+1));
}
}
}
return leaves;
}
Исключение AABB — одна из самых необычных вещей, которые я делаю (я продолжаю искать других людей, которые делают это только для того, чтобы найти равного и потерпеть неудачу), но я измерил до и после, и это действительно значительно сократило время, по крайней мере, на очень большие входные данные, чтобы существенно сжать узел дерева квадрантов и просто вычислять AABB на лету во время обхода. Пространство и время не всегда диаметрально противоположны. Иногда сокращение пространства также означает сокращение времени, учитывая, насколько в наши дни производительность зависит от иерархии памяти. Я даже ускорил некоторые операции реального мира, применяемые к массивным входным данным, сжимая данные в четыре раза по сравнению с используемой памятью и распаковывая их на лету.
Я не знаю, почему многие люди предпочитают кэшировать AABB: будь то удобство программирования или действительно быстрее в их случаях. Тем не менее, для структур данных, которые равномерно разделены по центру, таких как обычные деревья квадрантов и октодеревьев, я бы предложил измерять влияние исключения AABB и вычислять их на лету. Вы можете быть весьма удивлены. Конечно, имеет смысл хранить AABB для структур, которые не разделяются равномерно, например Kd-деревья и BVH, а также свободные деревья квадрантов.
Плавающая точка
Я не использую числа с плавающей запятой для пространственных индексов, и, возможно, поэтому я вижу улучшенную производительность, просто вычисляя AABB на лету со сдвигом вправо для деления на 2 и так далее. Тем не менее, по крайней мере, SPFP кажется очень быстрым в наши дни. Не знаю, так как не измерял разницу. Я просто предпочитаю использовать целые числа, хотя обычно работаю с входными данными с плавающей запятой (вершины сетки, частицы и т. д.). Я просто конвертирую их в целочисленные координаты для разделения и выполнения пространственных запросов. Я не уверен, есть ли какое-то серьезное преимущество в скорости. Это просто привычка и предпочтение, так как мне легче рассуждать о целых числах, не думая о денормализованной FP и тому подобном.
Центрированные AABB
Хотя я сохраняю ограничивающую рамку только для корня, полезно использовать представление, в котором хранится центр и половинный размер для узлов, а для запросов используется представление слева/сверху/справа/снизу, чтобы свести к минимуму объем арифметических операций.
Смежные дочерние элементы
Это также ключ, и если мы вернемся к представлению узла:
struct QuadNode
{
int32_t first_child;
...
};
Нам не нужно хранить массив дочерних элементов, потому что все 4 дочерних элемента непрерывны:
first_child+0 = index to 1st child (TL)
first_child+1 = index to 2nd child (TR)
first_child+2 = index to 3nd child (BL)
first_child+3 = index to 4th child (BR)
Это не только значительно уменьшает промахи в кеше при обходе, но также позволяет нам значительно уменьшить наши узлы, что еще больше уменьшает промахи в кеше, сохраняя только один 32-битный индекс (4 байта) вместо массива из 4 (16 байт).
Это означает, что если вам нужно передать элементы только в пару квадрантов родителя при его разделении, он все равно должен выделить все 4 дочерних листа для хранения элементов только в двух квадрантах, имея при этом два пустых листа в качестве дочерних. Тем не менее, компромисс более чем оправдан с точки зрения производительности, по крайней мере, в моих случаях использования, и помните, что узел занимает всего 8 байт, учитывая, насколько мы его сжали.
При освобождении потомков мы освобождаем все четыре одновременно. Я делаю это в постоянное время, используя индексированный свободный список, например:
![введите здесь описание изображения](https://i.stack.imgur.com/1oetf.png)
За исключением того, что мы объединяем фрагменты памяти, содержащие 4 смежных элемента, а не по одному. Это делает так, что нам обычно не нужно задействовать какие-либо выделения или освобождения кучи во время моделирования. Группа из 4 узлов помечается как освобожденная неделимо только для того, чтобы затем быть неделимо возвращена в последующем разделении другого конечного узла.
Отложенная очистка
Я не обновляю структуру дерева квадрантов сразу после удаления элементов. Когда я удаляю элемент, я просто спускаюсь по дереву к дочернему узлу (узлам), который он занимает, а затем удаляю элемент, но я пока не беспокоюсь о том, чтобы делать что-то еще, даже если листья становятся пустыми.
Вместо этого я делаю отложенную очистку следующим образом:
void Quadtree::cleanup()
{
// Only process the root if it's not a leaf.
SmallList<int> to_process;
if (nodes[0].count == -1)
to_process.push_back(0);
while (to_process.size() > 0)
{
const int node_index = to_process.pop_back();
QuadNode& node = nodes[node_index];
// Loop through the children.
int num_empty_leaves = 0;
for (int j=0; j < 4; ++j)
{
const int child_index = node.first_child + j;
const QuadNode& child = nodes[child_index];
// Increment empty leaf count if the child is an empty
// leaf. Otherwise if the child is a branch, add it to
// the stack to be processed in the next iteration.
if (child.count == 0)
++num_empty_leaves;
else if (child.count == -1)
to_process.push_back(child_index);
}
// If all the children were empty leaves, remove them and
// make this node the new empty leaf.
if (num_empty_leaves == 4)
{
// Push all 4 children to the free list.
nodes[node.first_child].first_child = free_node;
free_node = node.first_child;
// Make this node the new empty leaf.
node.first_child = -1;
node.count = 0;
}
}
}
Это вызывается в конце каждого кадра после перемещения всех агентов. Причина, по которой я выполняю такое отложенное удаление пустых листовых узлов в несколько итераций, а не сразу в процессе удаления одного элемента, заключается в том, что элемент A
может переместиться в узел N2
, сделав N1
пустым. Однако элемент B
может в том же кадре переместиться в N1
, снова сделав его занятым.
С отложенной очисткой мы можем справиться с такими случаями без ненужного удаления дочерних элементов только для того, чтобы добавить их обратно, когда другой элемент перемещается в этот квадрант.
Перемещение элементов в моем случае является простым: 1) удалить элемент, 2) переместить его, 3) повторно вставить его в дерево квадрантов. После того, как мы переместим все элементы и в конце кадра (не временного шага, может быть несколько временных шагов в каждом кадре), функция cleanup
выше вызывается для удаления дочерних элементов из родителя, который имеет 4 пустых листа в качестве дочерних, которые эффективно превращает этот родитель в новый пустой лист, который затем может быть очищен в следующем кадре с последующим вызовом cleanup
(или нет, если в него что-то вставлено или если братья и сестры пустого листа не пусты).
Давайте посмотрим на отложенную очистку визуально:
![введите здесь описание изображения](https://i.stack.imgur.com/FnfxP.png)
Начиная с этого, скажем, мы удаляем некоторые элементы из дерева, оставляя нам 4 пустых листа:
![введите здесь описание изображения](https://i.stack.imgur.com/7vh2y.png)
В этот момент, если мы вызовем cleanup
, он удалит 4 листа, если найдет 4 пустых дочерних листа, и превратит родителя в пустой лист, например:
![введите здесь описание изображения](https://i.stack.imgur.com/Cqliu.png)
Допустим, мы удалим еще несколько элементов:
![введите здесь описание изображения](https://i.stack.imgur.com/iuG5k.png)
... а затем снова вызовите cleanup
:
![введите здесь описание изображения](https://i.stack.imgur.com/TVwGG.png)
Если мы вызовем его еще раз, то получим следующее:
![введите здесь описание изображения](https://i.stack.imgur.com/8IB74.png)
... в этот момент сам корень превращается в пустой лист. Однако метод очистки никогда не удаляет корень (он удаляет только дочерние элементы). Опять же, основной смысл делать это отложенным таким образом и в несколько шагов состоит в том, чтобы уменьшить количество потенциальной избыточной работы, которая может выполняться за один временной шаг (которых может быть много), если бы мы делали все это немедленно каждый раз, когда элемент удаляется из дерево. Это также помогает распределить работу по кадрам, чтобы избежать заиканий.
TBH, я изначально применил эту технику отложенной очистки в игре для DOS, которую написал на C из-за чистой лени! Я не хотел возиться со спуском вниз по дереву, удалением элементов, а затем удалением узлов снизу вверх, потому что изначально я написал дерево для обхода сверху вниз (а не сверху вниз и снова вверх) и действительно думал, что это ленивое решение было компромиссом производительности (жертвование оптимальной производительности для более быстрого внедрения). Однако, много лет спустя, я на самом деле дошел до реализации удаления дерева квадрантов таким образом, что сразу начал удалять узлы, и, к моему удивлению, я фактически значительно замедлил его с более непредсказуемой и прерывистой частотой кадров. Отложенная очистка, несмотря на то, что изначально была вдохновлена моей программистской ленью, на самом деле (и случайно) оказалась очень эффективной оптимизацией для динамических сцен.
Односвязные индексные списки для элементов
Для элементов я использую это представление:
// Represents an element in the quadtree.
struct QuadElt
{
// Stores the ID for the element (can be used to
// refer to external data).
int id;
// Stores the rectangle for the element.
int x1, y1, x2, y2;
};
// Represents an element node in the quadtree.
struct QuadEltNode
{
// Points to the next element in the leaf node. A value of -1
// indicates the end of the list.
int next;
// Stores the element index.
int element;
};
Я использую узел элемента, который отделен от элемента. Элемент вставляется в дерево квадрантов только один раз, независимо от того, сколько ячеек он занимает. Однако для каждой ячейки, которую он занимает, вставляется узел элемента, который индексирует этот элемент.
Узел элемента представляет собой односвязный узел списка индексов в массиве, а также использует метод свободного списка, описанный выше. Это влечет за собой еще несколько промахов кеша из-за непрерывного хранения всех элементов листа. Однако, учитывая, что это дерево квадрантов предназначено для очень динамичных данных, которые перемещаются и сталкиваются на каждом временном шаге, обычно требуется больше времени обработки, чем экономия, чтобы найти идеально непрерывное представление для листовых элементов (вам фактически пришлось бы реализовать переменную распределитель памяти размером с размер, который действительно быстр, и это далеко не просто). Поэтому я использую односвязный список индексов, который позволяет использовать постоянный подход свободного списка к выделению/освобождению. Когда вы используете это представление, вы можете перенести элементы из разделенных родителей в новые листья, просто изменив несколько целых чисел.
SmallList<T>
О, я должен упомянуть об этом. Естественно, это помогает, если вы не выделяете кучу только для хранения временного стека узлов для нерекурсивного обхода. SmallList<T>
похож на vector<T>
, за исключением того, что он не будет включать выделение кучи, пока вы не вставите в него более 128 элементов. Это похоже на оптимизацию строк SBO в стандартной библиотеке C++. Это то, что я реализовал и использовал целую вечность, и это очень помогает убедиться, что вы используете стек, когда это возможно.
Представление в виде дерева
Вот представление самого дерева квадрантов:
struct Quadtree
{
// Stores all the elements in the quadtree.
FreeList<QuadElt> elts;
// Stores all the element nodes in the quadtree.
FreeList<QuadEltNode> elt_nodes;
// Stores all the nodes in the quadtree. The first node in this
// sequence is always the root.
std::vector<QuadNode> nodes;
// Stores the quadtree extents.
QuadCRect root_rect;
// Stores the first free node in the quadtree to be reclaimed as 4
// contiguous nodes at once. A value of -1 indicates that the free
// list is empty, at which point we simply insert 4 nodes to the
// back of the nodes array.
int free_node;
// Stores the maximum depth allowed for the quadtree.
int max_depth;
};
Как указано выше, мы сохраняем один прямоугольник для корня (root_rect
). Все суб-прямоугольники вычисляются на лету. Все узлы хранятся непрерывно в массиве (std::vector<QuadNode>
) вместе с элементами и узлами элементов (в FreeList<T>
).
FreeList<T>
Я использую структуру данных FreeList
, которая в основном представляет собой массив (и последовательность с произвольным доступом), которая позволяет вам удалять элементы из любого места в постоянное время (оставляя дыры, за которыми восстанавливаются при последующих вставках в постоянное время). Вот упрощенная версия, которая не занимается обработкой нетривиальных типов данных (не использует вызовы размещения новых или ручного уничтожения):
/// Provides an indexed free list with constant-time removals from anywhere
/// in the list without invalidating indices. T must be trivially constructible
/// and destructible.
template <class T>
class FreeList
{
public:
/// Creates a new free list.
FreeList();
/// Inserts an element to the free list and returns an index to it.
int insert(const T& element);
// Removes the nth element from the free list.
void erase(int n);
// Removes all elements from the free list.
void clear();
// Returns the range of valid indices.
int range() const;
// Returns the nth element.
T& operator[](int n);
// Returns the nth element.
const T& operator[](int n) const;
private:
union FreeElement
{
T element;
int next;
};
std::vector<FreeElement> data;
int first_free;
};
template <class T>
FreeList<T>::FreeList(): first_free(-1)
{
}
template <class T>
int FreeList<T>::insert(const T& element)
{
if (first_free != -1)
{
const int index = first_free;
first_free = data[first_free].next;
data[index].element = element;
return index;
}
else
{
FreeElement fe;
fe.element = element;
data.push_back(fe);
return static_cast<int>(data.size() - 1);
}
}
template <class T>
void FreeList<T>::erase(int n)
{
data[n].next = first_free;
first_free = n;
}
template <class T>
void FreeList<T>::clear()
{
data.clear();
first_free = -1;
}
template <class T>
int FreeList<T>::range() const
{
return static_cast<int>(data.size());
}
template <class T>
T& FreeList<T>::operator[](int n)
{
return data[n].element;
}
template <class T>
const T& FreeList<T>::operator[](int n) const
{
return data[n].element;
}
У меня есть один, который работает с нетривиальными типами и предоставляет итераторы и т. д., но гораздо сложнее. В наши дни я склонен больше работать с тривиально конструируемыми/разрушаемыми структурами в стиле C (используя только нетривиальные определяемые пользователем типы для высокоуровневых вещей).
Максимальная глубина дерева
Я предотвращаю слишком сильное разделение дерева, указав максимально допустимую глубину. Для быстрой симуляции, которую я набросал, я использовал 8. Для меня это имеет решающее значение, поскольку опять же, в визуальных эффектах я часто сталкиваюсь с патологическими случаями, включая контент, созданный художниками, с большим количеством совпадающих или перекрывающихся элементов, которые без ограничения максимальной глубины дерева могли бы хотят, чтобы он подразделяется на неопределенный срок.
Существует небольшая тонкая настройка, если вы хотите получить оптимальную производительность в отношении максимально допустимой глубины и количества элементов, которые вы разрешаете хранить в листе, прежде чем он разделится на 4 дочерних элемента. Я склоняюсь к тому, что оптимальные результаты достигаются при использовании максимум 8 элементов на узел до его разделения и максимальной глубины, установленной таким образом, чтобы наименьший размер ячейки немного превышал размер вашего среднего агента (в противном случае можно было бы вставить больше отдельных агентов). на несколько листьев).
Конфликт и запросы
Есть несколько способов обнаружения коллизий и запросов. Я часто вижу, как люди делают это так:
for each element in scene:
use quad tree to check for collision against other elements
Это очень просто, но проблема с этим подходом заключается в том, что первый элемент сцены может находиться в совершенно другом месте в мире, чем второй. В результате пути, по которым мы спускаемся по дереву квадрантов, могут быть совершенно случайными. Мы можем пройти один путь к листу, а затем снова захотеть пройти по тому же пути для первого элемента, например, для 50 000-го элемента. К этому времени узлы, участвующие в этом пути, могут быть уже вытеснены из кэша ЦП. Поэтому я рекомендую делать это следующим образом:
traversed = {}
gather quadtree leaves
for each leaf in leaves:
{
for each element in leaf:
{
if not traversed[element]:
{
use quad tree to check for collision against other elements
traversed[element] = true
}
}
}
Хотя это немного больше кода и требует, чтобы мы сохраняли набор битов traversed
или какой-то параллельный массив, чтобы избежать двойной обработки элементов (поскольку они могут быть вставлены более чем в один лист), это помогает убедиться, что мы спускаемся по одним и тем же путям вниз. дерево квадрантов по всему циклу. Это помогает сохранять вещи гораздо более удобными для кэширования. Кроме того, если после попытки переместить элемент на временном шаге он все еще полностью охвачен этим конечным узлом, нам даже не нужно снова возвращаться из корня (мы можем просто проверить только этот один лист).
Распространенные недостатки: чего следует избегать
Хотя существует множество способов ободрать кошку и найти эффективное решение, существует распространенный способ найти очень неэффективное решение. И я столкнулся со своей долей очень неэффективных деревьев квадрантов, kd-деревьев и октодеревьев за свою карьеру, работая в VFX. Мы говорим о гигабайте памяти, используемой только для разбиения сетки со 100 000 треугольников, при этом на сборку уходит 30 секунд, тогда как приличная реализация должна делать то же самое сотни раз в секунду и занимать всего несколько мегабайт. Есть много людей, разрабатывающих их для решения проблем, которые являются теоретическими волшебниками, но не уделяют особого внимания эффективности памяти.
Таким образом, самым распространенным запретом, который я вижу, является хранение одного или нескольких полноценных контейнеров с каждым узлом дерева. Под полноценным контейнером я подразумеваю что-то, что владеет, выделяет и освобождает свою собственную память, например:
struct Node
{
...
// Stores the elements in the node.
List<Element> elements;
};
И List<Element>
может быть списком в Python, ArrayList
в Java или C#, std::vector
в C++ и т. д.: некоторая структура данных, которая управляет своей собственной памятью/ресурсами.
Проблема здесь в том, что, хотя такие контейнеры очень эффективно реализованы для хранения большого количества элементов, все на всех языках крайне неэффективны, если вы создаете множество их экземпляров только для хранения нескольких элементов в каждый. Одна из причин заключается в том, что метаданные контейнера имеют тенденцию к взрывоопасному использованию памяти на таком детальном уровне одного узла дерева. Контейнеру может потребоваться хранить размер, емкость, указатель/ссылку на данные, которые он выделяет, и т. д., и все это для общих целей, поэтому он может использовать 64-битные целые числа для размера и емкости. В результате метаданные только для пустого контейнера могут быть 24 байта, что уже в 3 раза больше, чем полнота предложенного мной представления узла, и это только для пустого контейнера, предназначенного для хранения элементов в листьях.
Кроме того, каждый контейнер часто хочет либо выделить кучу/GC-аллоцировать при вставке, либо заранее потребовать еще больше предварительно выделенной памяти (в этот момент может потребоваться 64 байта только для самого контейнера). Таким образом, это либо становится медленным из-за всех выделений памяти (обратите внимание, что выделения GC изначально действительно быстры в некоторых реализациях, таких как JVM, но это только для начального цикла взрыва Eden), либо из-за того, что мы подвергаемся куче промахов кеша, потому что узлы настолько огромен, что почти не помещается в нижние уровни кеша ЦП при обходе или в оба.
Тем не менее, это очень естественная склонность и имеет интуитивно понятный смысл, поскольку мы теоретически говорим об этих структурах, используя такой язык, как Элементы хранятся в листьях, что предполагает хранение контейнера элементов в конечных узлах. К сожалению, это имеет взрывную стоимость с точки зрения использования памяти и обработки. Так что избегайте этого, если хотите создать что-то достаточно эффективное. Сделайте общий ресурс Node
и укажите (ссылайтесь) или индексируйте память, выделенную и сохраненную для всего дерева, а не для каждого отдельного узла. На самом деле элементы не должны храниться в листьях.
Элементы должны храниться в дереве, а конечные узлы должны индексировать или указывать на эти элементы.
Заключение
Уф, так что это основные вещи, которые я делаю, чтобы достичь того, что, надеюсь, считается достойным решением. Надеюсь, это поможет. Обратите внимание, что я нацелен на несколько продвинутый уровень для людей, которые уже реализовали деревья квадрантов хотя бы один или два раза. Если у вас есть какие-либо вопросы, не стесняйтесь стрелять.
Поскольку этот вопрос немного широк, я мог бы прийти и отредактировать его, а затем продолжать корректировать и расширять его с течением времени, если он не будет закрыт (мне нравятся вопросы такого типа, поскольку они дают нам повод написать о нашем опыте работы в поле, но сайту они не всегда нравятся). Я также надеюсь, что некоторые эксперты могут предложить альтернативные решения, из которых я могу извлечь уроки и, возможно, использовать для дальнейшего улучшения моего.
Опять же, деревья квадрантов на самом деле не являются моей любимой структурой данных для чрезвычайно динамичных сценариев столкновений, подобных этому. Так что, вероятно, мне есть чему поучиться у людей, которые предпочитают использовать деревья квадрантов для этой цели и много лет их настраивают и настраивают. В основном я использую quadtrees для статических данных, которые не перемещаются в каждом кадре, и для них я использую представление, сильно отличающееся от предложенного выше.
person
Community
schedule
18.01.2018
quadtree 2d collision detection
привел меня к этому вопросу, который, скорее всего, закроет ваш вопрос как дубликат. stackoverflow.com/questions/4981866/ - person akousmata   schedule 31.01.2017