Подтвердить что ты не робот

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

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

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

_MM_ALIGN16 struct Sphere
{
    // ....

    Vector3 point;
    float radius
};

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

_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
};

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

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

  • Какова проблема с определением операторов?

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

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

4b9b3361

Ответ 1

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

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

  • Динамическое распределение. Так много для легкой части. Проблема заключается в том, что С++ использует на самом низком уровне функцию распределения памяти с типом-агностиком для распределения любой динамической памяти. Это гарантирует только правильное выравнивание для всех стандартных типов, которое может быть 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, но я не знаю о последствиях этого подхода, В худшем случае это должно просто привести к потере памяти, но опять же вы обычно не выделяете небольшие объекты динамически в С++ (хотя std::list и std::map могут по-другому думать об этом).

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

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

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

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


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

Ответ 2

В принципе, вам нужно убедиться, что ваши векторы правильно выровнены, потому что типы векторов SIMD обычно имеют более высокие требования к выравниванию, чем любые встроенные типы.

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

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

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

Ответ 3

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

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

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