SSE, внутренности и выравнивание

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

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

_MM_ALIGN16 struct Sphere
{
// ....

Vector3 point;
float radius
};

Это, казалось, решило проблему сначала. Но после изменения какого-то кода моя программа снова стала аварийно завершаться. Я искал в Интернете еще немного и нашел блог статья. Я попробовал то, что автор, Эрнст Хот, сделал, чтобы решить проблему, и это работает и для меня. Я добавил новые и удаляю операторы в мои классы следующим образом:

_MM_ALIGN16 struct Sphere
{
// ....

void *operator new (unsigned int size)
{ return _mm_malloc(size, 16); }

void operator delete (void *p)
{ _mm_free(p); }

Vector3 point;
float radius
};

Эрнст упоминает, что этот подход также может быть проблематичным, но он просто ссылается на форум, который больше не существует, не объясняя, почему он может быть проблематичным.

Итак, мои вопросы:

  1. В чем проблема с определением операторов?

  2. Почему не добавляет _MM_ALIGN16 к определению класса достаточно?

  3. Какой лучший способ справиться с проблемами выравнивания, возникающими при использовании встроенных компонентов SSE?

23

Решение

Прежде всего вы должны заботиться о двух типах выделения памяти:

  • Статическое распределение. Для правильного выравнивания автоматических переменных вашему типу нужна правильная спецификация выравнивания (например, __declspec(align(16)), __attribute__((aligned(16)))или ваш _MM_ALIGN16). Но, к счастью, это нужно только в том случае, если требования выравнивания, заданные членами типа (если они есть), недостаточны. Так что тебе это не нужно Sphere, учитывая, что ваш Vector3 уже выровнен правильно. И если ваш Vector3 содержит __m128 член (что весьма вероятно, в противном случае я бы предложил сделать это), то вам даже не нужно это для Vector3, Таким образом, вам обычно не нужно связываться с определенными атрибутами компилятора.

  • Динамическое распределение. Так много для легкой части. Проблема в том, что C ++ использует на низшем уровне довольно независимую от типа функцию выделения памяти для выделения любой динамической памяти. Это только гарантирует правильное выравнивание для всех стандартных типов, которые могут иметь размер 16 байт, но это не гарантируется.

    Чтобы это компенсировать, нужно перегрузить встроенный operator new/delete реализовать собственное распределение памяти и использовать выровненную функцию выделения под капотом вместо старого доброго malloc, перегрузка operator new/delete это отдельная тема, но это не так сложно, как может показаться на первый взгляд (хотя вашего примера недостаточно), и вы можете прочитать об этом в этот отличный вопрос FAQ.

    К сожалению, вы должны сделать это для каждого типа, который имеет любого члена, нуждающегося в нестандартном выравнивании, в вашем случае оба Sphere а также Vector3, Но то, что вы можете сделать, чтобы сделать это немного проще, — это просто создать пустой базовый класс с соответствующими перегрузками для этих операторов, а затем просто извлечь все необходимые классы из этого базового класса.

    Большинство людей иногда забывают о том, что стандартный распределитель std::alocator использует глобальный operator new для всего распределения памяти, так что ваши типы не будут работать со стандартными контейнерами (и std::vector<Vector3> не редкость случай использования). Что вам нужно сделать, это сделать свой собственный собственный соответствующий распределитель и использовать его. Но для удобства и безопасности на самом деле лучше просто специализироваться std::allocator для вашего типа (возможно, просто извлекая его из вашего пользовательского распределителя), чтобы он всегда использовался, и вам не нужно заботиться об использовании правильного распределителя каждый раз, когда вы используете std::vector, К сожалению, в этом случае вам придется снова специализировать его для каждого выровненного типа, но в этом помогает маленький злой макрос.

    Кроме того, вы должны следить за другими вещами, используя глобальные operator new/delete вместо вашего обычного, как std::get_temporary_buffer а также std::return_temporary_bufferи заботиться о тех, кто в случае необходимости.

К сожалению, я думаю, что пока нет лучшего подхода к этим проблемам, если только вы не находитесь на платформе, которая изначально соответствует 16 и знать об этом. Или вы можете просто перегрузить глобальный operator new/delete всегда выравнивать каждый блок памяти по 16 байтов и не заботиться о выравнивании каждого отдельного класса, содержащего член SSE, но я не знаю о последствиях этого подхода. В худшем случае это должно привести к потере памяти, но, опять же, вы обычно не выделяете небольшие объекты динамически в C ++ (хотя std::list а также std::map может думать по-другому об этом).

Итак, подведем итог:

  • Заботьтесь о правильном выравнивании статической памяти, используя такие вещи, как __declspec(align(16)), но только если об этом не заботится ни один участник, как это обычно бывает.

  • перегрузка operator new/delete для каждого типа, имеющего член с нестандартными требованиями выравнивания.

  • Сделать cunstom стандартно-совместимый распределитель для использования в стандартных контейнерах выровненных типов, или, еще лучше, специализироваться std::allocator для каждого выровненного типа.


Напоследок несколько общих советов. Часто вы получаете прибыль только от SSE в вычислительных блоках, когда выполняете много векторных операций. Чтобы упростить все эти проблемы выравнивания, особенно проблемы ухода за выравниванием каждого типа, содержащего Vector3Это может быть хорошим подходом для создания специального векторного типа SSE и использования его только внутри длинных вычислений, используя обычный вектор без SSE для хранения и переменных-членов.

19

Другие решения

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

Это требует выполнения следующих действий:

  1. Удостоверься что Vector3 правильно выровнен, когда он находится в стеке или элемент структуры. Это делается путем применения __attribute__((aligned(32))) в Vector3 класс (или любой другой атрибут, поддерживаемый вашим компилятором). Обратите внимание, что вам не нужно применять атрибут к структурам, содержащим Vector3это не является необходимым и недостаточным (то есть нет необходимости применять его к Sphere).

  2. Удостоверься что Vector3 или его ограждающая структура правильно выровнена при использовании выделения кучи. Это делается с помощью posix_memalign() (или аналогичная функция для вашей платформы) вместо использования обычного malloc() или же operator new() потому что последние два выравнивают память для встроенных типов (обычно 8 или 16 байтов), что не гарантируется для SIMD-типов.

2

  1. Проблема с операторами заключается в том, что сами их недостаточно. Они не влияют на распределение стека, для которого вам все еще нужно __declspec(align(16)),

  2. __declspec(align(16)) влияет на то, как компилятор помещает объекты в память, если и только если у него есть выбор. Для новых объектов у компилятора нет другого выбора, кроме как использовать память, возвращаемую operator new,

  3. В идеале используйте компилятор, который обрабатывает их изначально. Нет теоретической причины, почему к ним нужно относиться иначе double, Иначе, прочитайте документацию компилятора для обходных путей. Каждый компилятор с ограниченными возможностями будет иметь свой собственный набор проблем и, следовательно, свой собственный набор обходных путей.

1