Я часто читаю в документации игрового движка ECS, которая является хорошей архитектурой для разумного использования кэша процессора.
Но я не могу понять, как мы можем извлечь выгоду из кэша процессора.
Если компоненты сохраняются в массиве (или пуле) в непрерывной памяти, это хороший способ использовать кэш процессора, НО, только если мы последовательно читаем компоненты.
Когда мы используем системы, им нужен список объектов, которые представляют собой список объектов, имеющих компоненты с определенными типами.
Но эти списки дают компоненты случайным образом, а не последовательно.
Итак, как спроектировать ECS, чтобы максимизировать попадание в кеш?
РЕДАКТИРОВАТЬ :
Например, физической системе требуется список объектов для объекта, который имеет компоненты RigidBody и Transform (существует пул для RigidBody и пул для компонентов Transform).
Так что его цикл обновления сущностей будет таким:
for (Entity eid in entitiesList) {
// Get rigid body component
RigidBody *rigidBody = entityManager.getComponentFromEntity<RigidBody>(eid);
// Get transform component
Transform *transform = entityManager.getComponentFromEntity<Transform>(eid);
// Do something with rigid body and transform component
}
Проблема заключается в том, что компонент RigidBody объекта entity1 может находиться в индексе 2 его пула, а компонент Tranform объекта entity1 - в индексе 0 его пула (поскольку некоторые объекты могут иметь некоторые компоненты, а не другие, и из-за добавления / удаления объектов / компоненты случайным образом).
Таким образом, даже если компоненты находятся в памяти непрерывно, они читаются случайным образом, и поэтому в них будет больше промахов, не так ли?
Разве есть способ предварительной выборки следующих компонентов в цикле?
Ответы:
Статья Мика Уэста полностью объясняет процесс линеаризации данных компонентов сущности. Несколько лет назад он работал в серии Tony Hawk на гораздо менее впечатляющем оборудовании, чем у нас сегодня, чтобы значительно повысить производительность. Он в основном использовал глобальные, предварительно распределенные массивы для каждого отдельного типа данных объекта (позиция, оценка и тому подобное) и ссылается на каждый массив в отдельной фазе своей общесистемной
update()
функции. Можно предположить, что данные для каждой сущности будут иметь одинаковый индекс массива в каждом из этих глобальных массивов, поэтому, например, если проигрыватель создается первым, он может иметь свои данные[0]
в каждом массиве.Еще более специфичные для оптимизации кэша, слайды Кристер Эрикссон для C и C ++.
Чтобы дать немного больше подробностей, вы должны попытаться использовать смежные блоки памяти (наиболее легко распределяемые как массивы) для каждого типа данных (например, позиции, xy и z), чтобы обеспечить хорошую локальность ссылок, используя каждый такой блок данных в разных
update()
этапы ради временной локализации, т. е. чтобы гарантировать, что кэш не очищен с помощью аппаратного алгоритма LRU, прежде чем вы повторно используете любые данные, которые вы намерены использовать повторно, в рамках данногоupdate()
вызова. Как вы и предполагали, вам не нужно выделять ваши сущности и компоненты как отдельные объектыnew
, так как данные разных типов в каждом экземпляре сущности будут затем чередоваться, уменьшая локальность ссылок.Если у вас есть взаимозависимости между компонентами (данными), так что вы абсолютно не можете позволить себе отделить некоторые данные от связанных с ними данных (например, Transform + Physics, Transform + Renderer), тогда вы можете выбрать репликацию данных Transform в массивах Physics и Renderer. , гарантируя, что все соответствующие данные соответствуют ширине строки кэша для каждой операции, критичной к производительности.
Помните также, что кэш L2 и L3 (если вы можете предположить, что это для вашей целевой платформы) делают многое для смягчения проблем, которые могут возникнуть в кэше L1, таких как ограничение ширины строки. Таким образом, даже при пропадании L1 это защитные сети, которые чаще всего предотвращают обратные вызовы в основную память, которые на несколько порядков медленнее, чем выноски на любой уровень кеша.
Примечание по записи данных Запись не вызывает в основной памяти. По умолчанию в современных системах включено кэширование с обратной записью : запись значения записывает его только в кеш (изначально), а не в основную память, поэтому вы не будете узким местом в этом. Только когда данные запрашиваются из основной памяти (не будет происходить, пока они находятся в кеше) и устарели, основная память будет обновляться из кеша.
источник
std::vector
новичком в C ++: в основном это динамически изменяемый размер массива, и, следовательно, он также является смежным (де-факто в старых версиях C ++ и де-юре в новых версиях C ++). Некоторые реализацииstd::deque
также являются "достаточно смежными" (хотя не Microsoft).