Правильно ли мое понимание преимуществ/недостатков AoS vs SoA?

я недавно читал о AoS vs SoA конструкция и ориентированный на данные дизайн. Странно трудно найти информацию о том или ином, и то, что я нашел, похоже, предполагает большее понимание функциональности процессора, чем у меня есть. Тем не менее, то, что я понимаю по первой теме, в частности, приводит к некоторым вопросам, на которые, я думаю, я должен быть в состоянии понять ответы.

во-первых, чтобы убедиться, что я не основываясь на моем понимании ложной предпосылки, моем понимании функциональности и плюсов и минусов AoS vs SoA, применительно к коллекции записей "человек" с полями " имя " и "возраст", связанными с ними:

структура массивов

  • хранит данные как одну структуру, состоящую из нескольких массивов, например, как People объект с полями Names как массив строк и Ages как массив целых чисел.
  • информация ибо, скажем, третье лицо в списке будет дано чем-то вроде People.Names[2] и People.Ages[2]
  • плюсы:
    • при работе только с некоторыми данными из многих записей "человек", только эти данные должны быть загружены из памяти.
    • указанные данные хранятся в однородном виде, что позволяет кэш лучше использовать инструкции SIMD в большинстве таких ситуаций.
  • минусы: - Когда несколько полей должны быть доступны на как только, вышеуказанные преимущества идут прочь. - Доступ ко всем данным для одного или нескольких объектов становится менее эффективным. - Большинство языков программирования требуют гораздо более подробного и трудного для чтения/записи кода, поскольку нет явной структуры "человек".

массив структур

  • хранит данные в виде нескольких структур, каждая из которых имеет полный набор полей, которые сами хранятся в массиве всех таких структур, например a People массив Person объекты, которые имеют Name как строковое поле и Age как целое поле.
  • информация для третьего лица будет предоставлена чем-то вроде People[2].Name и People[2].Age
  • плюсы:
    • код структурирован вокруг более простой ментальной модели, при этом косвенность абстрагируется.
    • одиночные записи легки для того чтобы достигнуть и работать С.
    • в наличии Person структура делает написание кода в большинстве программ языки гораздо проще.
  • минусы:
    • при работе только с некоторыми данными из большого количества записей весь набор структур должен быть загружен в память, включая нерелевантные данные.
    • массив структур неоднороден, что в таких ситуациях ограничивает преимущество, которое может быть предоставлено инструкциями SIMD.

длинные и короткие вроде что, предположим ради спора, что узким местом для производительности доступа к данным и простота кодирования не имеет значения, если вы почти исключительно нужно получить доступ к одному полю за раз на большой объем данных SOA-это, вероятно, будет более производительным, а если вам часто приходится выходить несколько полей из одного объекта или сделки с отдельными объектами, а не много и сразу, АОС будет более производительным.

что сказал, что я читал, кажется, мутят изображение. Во-первых, несколько источников заявили, что SoA требует индексированной адресации, которая, как утверждается, неэффективна. Я не могу понять этого и не могу найти никаких объяснений. Мне кажется, что AoS и SoA требуют точно таких же операций для доступа к любому конкретному фрагменту данных, хотя и в разных порядках, за исключением того, что SoA требует дополнительного указателя (возможно, более одного, в зависимости от типа используемой структуры). Немного упрощая, чтобы получить возраст пятого человек в моем приведенном выше примере под AoS вы сначала получите указатель на массив, добавьте к нему 4, получите указатель структуры в этом элементе массива, добавьте к нему размер указателя строки, так как возраст является вторым полем, а затем получите доступ к целому числу в этом указателе. В SoA вы получите указатель на структуру и добавите к ней размер указателя строкового массива, чтобы добраться до списка возрастов, затем получите указатель на список целых чисел, хранящихся там, и добавьте к нему 4, а затем получите целое число хранить там.

во-вторых, мне не ясно, в какой степени преимущества SoA зависят от конкретных архитектур ЦП. С одной стороны, то, что я понимаю о преимуществах, как описано выше, не зависит от какой-либо конкретной архитектуры, за исключением того, что инструкции SIMD могут предоставить дополнительные преимущества, недоступные в AoS в некоторых случаях. С другой стороны, я видел утверждения, что преимущества SoA могут быть ограничены в зависимости от количества полос, доступных в конкретном SIMD архитектура. Опять же, это, по-видимому, влияет только на дополнительное преимущество, которое инструкции SIMD могут предоставить над более общим преимуществом кэша.

наконец, я видел утверждение, что SoA может потребовать больше способов кэша при обходе данных. Я не совсем уверен, что такое способы кэширования или что, если что-то, конкретно подразумевается под "обходом" данных. Мое лучшее предположение заключается в том, что "пути кэша" либо относятся, либо коррелируют с количеством потенциальных столкновений в ассоциативном кэше, и что это относится ко второй афере, о которой я упоминал выше.

1 ответов


"обход" просто означает цикл над данными.

и да, вы правы о способах кэширования и столкновениях. 64B (размер строки кэша) блоки памяти, которые смещены друг от друга большой мощностью 2 карты в один и тот же набор, и, таким образом, конкурируют друг с другом за способы в этом наборе, вместо того, чтобы кэшироваться в разных наборах. (например, кэш данных Intel L1-32kiB, 8-полосный ассоциативный, с 64b линиями. Есть 32k / 64 = 512 строк, сгруппированных в 512/64 = 64 наборы.

загрузка 9 элементов смещены друг от друга на 4kiB (64B/line * 64 sets, не случайно размер страницы) выселит первый.

кэши L2 и L3 более ассоциативны, как 16 или 24 способа, но все еще восприимчивы к "сглаживанию", как это, точно так же, как хэш-таблица, где есть много спроса на некоторые наборы (ведра) и нет спроса на другие наборы (ведра). Для кэшей ЦП "хэш-функция" почти всегда должна использовать некоторые из адресных битов в качестве индекса и игнорируйте другие биты. (Высокие биты адреса используются в качестве тега, чтобы определить, действительно ли какой-либо способ в наборе кэширует запрошенный блок, а низкие биты используются для выбора байтов в строке кэша.)


Я думаю, что преимущество SoA в основном от SIMD (авто-векторизация или руководство), но также, если вы склонны перебирать свои данные, глядя только на одно или два поля из большинства структур, и только доступ к остальным в редких случаях, когда вы найдете интересный на основе одного члена.

гибридный подход с отдельными массивами для каждой вещи (или группы вещей), которую вы смотрите вместе, может иметь смысл, а остальные данные для каждого объекта в массиве структур. Я представляю себе линейный цикл поиска, где большинство объектов отклоняются на основе просмотра одного int поле, но для немногих объектов, которые проходят этот тест, вы смотрите на все поля.

группировка полей, которые в основном доступны вместе, дает вам преимущество пространственной локальности для этих обращений, все еще позволяя циклам поиска, которые проверяют цикл ключевого поля над смежной памятью (а не с большим шагом).


в настоящее время я экспериментирую с макетом, который чередуется в группах векторного размера SIMD. Большая часть кода, который пересекает данные, требует всех полей от каждого объекта, и это означает, что циклу нужен только один указатель, и вся память выделяется как один блок.

этот для масок обнаружения столкновений (в 2D-космической игре (бесконечное небо), где все это столкновение между сегментом линии и контуром корабля (прослеживается автоматически из спрайта), а не между двумя полигонами). Вот!--34-->оригинал что проциклили вектор double x,y пары (и использовали некоторые (не встроенные!) функции для работы с ними в качестве вектора SIMD 16B,часто с медленным SSE3 горизонтальным-добавить инструкции и тому подобное :( ).

SSE2 / SSE3 на XY пары, вероятно, лучше, чем ничего, если вы не можете изменить макет данных, но изменение макета удаляет все перетасовки для выполнения 4 перекрестных продуктов параллельно. посмотреть слайды из этого вступления SIMD (SSE) в Insomniac Games (GDC 2015). Он начинается с очень простых вещей для людей, которые ничего не делали с SIMD раньше, и объясняет, насколько полезны структуры массивов. К концу он добирается до промежуточных / продвинутых методов SSE, поэтому это стоит листать, даже если вы уже знаете некоторые вещи SIMD. См. также sse tag wiki для некоторых других ссылок.


в любом случае, это структура данных interleave, которую я придумал:

class Mask {
...

struct xy_interleave {
    static constexpr unsigned vecSize = 4;
    static constexpr unsigned alignMask = vecSize-1;
    alignas(64) float x[vecSize];
    float y[vecSize];
    // TODO: reduce cache footprint by calculating this on the fly, maybe with an unaligned load?
    float dx[vecSize]; // next - current;   next.x = x+dx
    float dy[vecSize];
};
std::vector<xy_interleave> outline_simd;

}

тогда я могу зациклиться на нем с такими вещами, как (реальный код здесь: это мой незавершенный неочищенный код, который не готов к отправке вверх по течению)

__m128 minus_point_ps = _mm_cvtpd_ps(-point);    // + is commutative, which helps the compiler with AVX
const __m128 minus_px = _mm_set1_ps(minus_point_ps[0]);
const __m128 minus_py = _mm_set1_ps(minus_point_ps[1]);
const __m128 range2 = _mm_set1_ps(float(range*range));

for(const xy_interleave &curr : outline_simd)
{
    __m128 dx = _mm_load_ps(curr.x) + minus_px;
    __m128 dy = _mm_load_ps(curr.y) + minus_py;
    // this is using GNU Vector Extensions for + and *, instead of _mm_add_ps and _mm_mul_ps, since GNU C++ defines __m128 in terms of __v4sf
    __m128 cmp = _mm_cmplt_ps(dx*dx - range2, dy*dy);  // transform the inequality for more ILP
    // load the x and y fields from this group of 4 objects, all of which come from the same cache line.

    if(_mm_movemask_ps(cmp))
        return true;
}

это компилируется в действительно красивые петли asm, только с один указатель зацикливается на std:: vector и вектор загружается из постоянных смещений относительно этого указателя цикла.

однако скалярные резервные циклы над теми же данными менее красивы. (И на самом деле я использую такие петли (с j+=4) В вручную векторизованных частях тоже, поэтому я могу изменить чередование, не нарушая код. Он компилируется полностью или превращается в развернутый).

// TODO: write an iterator or something to make this suck less
for(const xy_interleave &curr : outline_simd)
    for (unsigned j = 0; j < curr.vecSize; ++j)
    {
        float dx = curr.x[j] - px;
        float dy = curr.y[j] - py;
        if(dx*dx + dy*dy < range2)
            return true;
    }

к сожалению, мне не повезло получить gcc или clang авто-векторизуйте это, даже для простых случаев без условий (например,просто найдите минимальный диапазон от запроса x, y до любой точки в маске столкновения, вместо того, чтобы проверять, находится ли точка в пределах диапазона).


я мог бы отказаться от этой идеи и пойти с отдельными массивами x и y. (Может быть, упакованы голова к хвосту в том же std::vector<float> (с выровненным распределителем), чтобы сохранить его частью одного распределения, но это все равно будет означать, что циклам понадобятся отдельные указатели x и y, потому что смещение между x и y для данной вершины будет переменная времени выполнения, а не константа времени компиляции.)

имея все xs contiguous будет большой помощью, если я хочу прекратить хранить x[i+1]-x[i], и вычислить его на лету. С моим макетом мне нужно было бы перетасовать между векторами, а не просто делать несогласованное смещение на 1 float.

это, надеюсь, также позволит компилятору автоматически векторизовать некоторые функции (например, для ARM или для AVX/AVX2 с более широким векторные иллюстрации.)

конечно, ручная векторизация выиграет здесь, так как я делаю такие вещи, как XORing плавает вместе, потому что я забочусь только об их знаке как значении истины, вместо того, чтобы делать сравнение, а затем XORing результат сравнения. (Мое тестирование до сих пор показало, что обработка отрицательного 0 как отрицательного все еще дает правильные результаты для Mask:: Intersect, но любой способ выразить его в C будет следовать правилам IEEE, где x >= 0 это верно для x=-0.).


если вам почти исключительно нужно получить доступ к одному полю за раз на большом количестве данных AoS, вероятно, будет более эффективным, а если вам часто нужно получить доступ к нескольким полям из одного объекта или иметь дело с отдельными объектами, а не со многими сразу, SoA будет более эффективным.

у вас это совсем наоборот. Это опечатка? Группировка всех foo[i].key поля в foo.key[i] массив означает, что они все упакованы вместе в кэше, поэтому доступ только к одному полю во многих объектах означает, что вы используете все 64 байта каждой строки кэша, которую вы касаетесь.

вы получили это правильно ранее, когда вы написали

при работе только с некоторыми данными из многих записей "человек", только эти данные должны быть загружены в память.

(кроме того, я думаю, что вы имеете в виду" из " памяти (в кэш), если вы не говорите о файле с сопоставлением памяти и страницах ошибок с диска в память.)


индексированные режимы адресации:

в ситуации, когда вы смотрите на два или три поля в каждом объекте, макет SoA свяжет больше регистров, содержащих отдельные базовые адреса для каждого отдельного массива, который вы зацикливаете.

С несколькими указателями вы захотите использовать режимы адресации, такие как [reg1 + 4*reg2] на x86, или вам нужно будет отдельно увеличить кучу различные указатели внутри вашего цикла. Индексированные режимы адресации потенциально немного медленнее на Intel SnB-family, потому что они не может оставаться микро-слитым с ALU uops в ядре вне порядка (только в декодерах и кэше uop). Skylake может держать их микро-сплавленными, но необходимо дальнейшее тестирование, чтобы узнать, когда Intel сделала это изменение. Возможно, с Broadwell, когда трехвходовые инструкции за пределами FMA (например, CMOV и ADC) декодируются в один uop, но это чистая догадка. Тестирование на Haswell и Broadwell необходимо.