Я пытаюсь получить хорошее представление о дизайне, ориентированном на данные, и о том, как лучше всего программировать с учетом кеша. В принципе, есть два сценария, которые я не могу точно решить, какой из них лучше и почему — лучше ли иметь вектор объектов или несколько векторов с атомарными данными объектов?
А) Пример вектора объектов
struct A
{
GLsizei mIndices;
GLuint mVBO;
GLuint mIndexBuffer;
GLuint mVAO;
size_t vertexDataSize;
size_t normalDataSize;
};
std::vector<A> gMeshes;
for_each(gMeshes as mesh)
{
glBindVertexArray(mesh.mVAO);
glDrawElements(GL_TRIANGLES, mesh.mIndices, GL_UNSIGNED_INT, 0);
glBindVertexArray(0);
....
}
Б) Векторы с атомными данными
std::vector<GLsizei> gIndices;
std::vector<GLuint> gVBOs;
std::vector<GLuint> gIndexBuffers;
std::vector<GLuint> gVAOs;
std::vector<size_t> gVertexDataSizes;
std::vector<size_t> gNormalDataSizes;
size_t numMeshes = ...;
for (index = 0; index++; index < numMeshes)
{
glBindVertexArray(gVAOs[index]);
glDrawElements(GL_TRIANGLES, gIndices[index], GL_UNSIGNED_INT, 0);
glBindVertexArray(0);
....
}
Какой из них более эффективен при использовании памяти и кэш-памяти, что приводит к уменьшению количества кеш-памяти и повышению производительности, и почему?
В зависимости от уровня кеша, о котором вы говорите, кеш работает следующим образом:
Поэтому наивно задаваемые вопросы:
Итак, я бы ожидал, что B будет быстрее для этого кода. Тем не мение:
struct
, Так сделай это. Предположительно на самом деле это не единственный доступ к данным в вашей программе, и другие обращения могут повлиять на производительность двумя способами: время, которое они на самом деле занимают, и заполнение кеша данными, которые вам нужны.Я понимаю, что это частично основано на мнении, а также на том, что это может быть причиной преждевременной оптимизации, но ваш первый вариант определенно имеет лучшую эстетику. Это один вектор против шести — в моих глазах нет соревнований.
Для производительности кеша это должно быть лучше. Это потому, что альтернатива требует доступа к двум различным векторам, что разделяет доступ к памяти каждый раз, когда вы визуализируете сетку.
При структурном подходе сетка по сути является автономным объектом и правильно не подразумевает никакого отношения к другим сеткам. При рисовании вы получаете доступ только к тот сетка, а при рендеринге все меш, вы делаете по одному в кеш-дружественной манере. Да, вы будете кушать кеш быстрее, потому что ваши векторные элементы больше, но вы не будете оспаривать это.
Вы также можете позже найти другие преимущества от использования этого представления. т.е. если вы хотите хранить дополнительные данные о сетке. Добавление дополнительных данных в большее количество векторов быстро загромождает ваш код и увеличивает риск ошибочных ошибок, а вносить изменения в структуру — тривиально.
Я рекомендую профилирование с любым перфорация или же OProfile и опубликовать свои результаты здесь (при условии, что вы работаете в linux), включая количество элементов, с которыми вы перебирались, общее количество итераций и оборудование, на котором вы тестировали.
Если бы мне пришлось угадывать (а это только предположение), я бы подозревал, что первый подход мог бы быть быстрее из-за локальности данных в каждой структуре, и, надеюсь, ОС / аппаратное обеспечение может предварительно выбрать для вас дополнительные элементы. Но опять же, это будет зависеть от размера кеша, размера строки кеша и других аспектов.
Определение «лучше» тоже интересно. Вы ищете общее время для обработки N элементов, низкую дисперсию в каждом образце, минимальное количество кеш-пропусков (на которое будут влиять другие процессы, работающие в вашей системе) и т. Д.
Не забывайте, что с векторами STL вы также зависите от распределителя … например он может в любое время принять решение о перераспределении массива, что сделает ваш кеш недействительным. Еще один фактор, чтобы попытаться изолировать, если вы можете!
Зависит от ваших шаблонов доступа. Ваша первая версия AoS (массив структур), второй SoA (структура массивов).
SoA имеет тенденцию использовать меньше памяти (если вы не храните так мало элементы, что накладные расходы массивов на самом деле нетривиальны), если есть какие-либо дополнения структуры, которые вы обычно получаете в представлении AoS. Он также имеет гораздо большую PITA для кодирования, так как вы должны поддерживать / синхронизировать параллельные массивы.
AoS имеет тенденцию превосходить произвольный доступ. В качестве примера, для простоты, скажем, каждый элемент помещается в строку кэша и правильно выравнивается (например, размер и выравнивание 64 байта). В этом случае, если вы случайно получаете доступ к nth
элемент, вы получите все соответствующие данные для элемента в одной строке кэша. Если вы использовали SoA и распределяли эти поля по отдельным массивам, вам пришлось бы загружать память в несколько строк кэша, чтобы загрузить данные для этого одного элемента. И поскольку мы обращаемся к данным в случайном порядке, мы вообще не получаем большой пользы от пространственной локализации, поскольку следующий элемент, к которому мы собираемся получить доступ, может быть где-то в памяти.
Тем не менее, SoA имеет тенденцию превосходить последовательный доступ главным образом потому, что зачастую меньше данных для загрузки в кэш ЦП, во-первых, для всего последовательного цикла, потому что это исключает заполнение структуры и холодные поля. Под холодными полями я подразумеваю поля, к которым вам не нужно обращаться в определенном последовательном цикле. Например, физическая система может не заботиться о полях частиц, связанных с тем, как частица выглядит для пользователя, таких как цвет и дескриптор спрайта. Это неактуальные данные. Это касается только положения частиц. SoA позволяет избежать загрузки этих не относящихся к делу данных в строки кэша. Это позволяет вам загружать как можно больше релевантных данных за раз в строку кэша, поэтому вы получаете меньше обязательных пропусков кэша (а также сбоев страниц при достаточно больших данных) с помощью SoA.
Это также охватывает только шаблоны доступа к памяти. С представителями SoA вы также можете писать более эффективные и простые SIMD-инструкции. Но опять же это в основном подходит для последовательный доступ.
Вы также можете смешать две концепции. Вы можете использовать AoS для горячих полей, к которым часто обращаются вместе в произвольном порядке, затем поднимать холодные поля и хранить их параллельно.