Ответ 1
Хорошо, если вы хотите использовать SIMD-расширения, хороший подход - использовать встроенные средства SSE (конечно же, непременно оставайтесь в стороне от встроенной сборки, но, к счастью, вы не указали ее как альтернативу). Но для чистоты вы должны инкапсулировать их в класс с хорошим вектором с перегруженными операторами:
struct aligned_storage
{
//overload new and delete for 16-byte alignment
};
class vec4 : public aligned_storage
{
public:
vec4(float x, float y, float z, float w)
{
data_[0] = x; ... data_[3] = w; //don't use _mm_set_ps, it will do the same, followed by a _mm_load_ps, which is unneccessary
}
vec4(float *data)
{
data_[0] = data[0]; ... data_[3] = data[3]; //don't use _mm_loadu_ps, unaligned just doesn't pay
}
vec4(const vec4 &rhs)
: xmm_(rhs.xmm_)
{
}
...
vec4& operator*=(const vec4 v)
{
xmm_ = _mm_mul_ps(xmm_, v.xmm_);
return *this;
}
...
private:
union
{
__m128 xmm_;
float data_[4];
};
};
Теперь хорошо, из-за анонимного объединения (UB, я знаю, но покажу мне платформу с SSE, где это не работает) вы можете использовать стандартный массив float всякий раз, когда это необходимо (например, operator[]
или инициализация (не используйте _mm_set_ps
)) и при необходимости используйте SSE. С современным встроенным компилятором инкапсуляция приходит, вероятно, без затрат (я был довольно удивлен, насколько хорошо VC10 оптимизировал инструкции SSE для кучи вычислений с этим векторным классом, не опасаясь ненужных перемещений во временные переменные памяти, поскольку VC8, казалось, даже нравился без инкапсуляции).
Единственным недостатком является то, что вам нужно позаботиться о правильном выравнивании, так как не выровненные векторы ничего не покупают и могут даже быть медленнее, чем не SSE. Но, к счастью, требование выравнивания __m128
будет распространяться на vec4
(и любой окружающий класс), и вам просто нужно позаботиться о динамическом распределении, для которого С++ имеет хорошие возможности. Вам просто нужно создать базовый класс, функции которого operator new
и operator delete
(во всех вкусах, конечно) перегружены должным образом и из которых будет получен ваш векторный класс. Разумеется, для использования вашего типа со стандартными контейнерами вам также необходимо специализироваться std::allocator
(и, возможно, std::get_temporary_buffer
и std::return_temporary_buffer
для полноты), так как в противном случае он будет использовать глобальный operator new
.
Но реальным недостатком является то, что вам также нужно уделить внимание динамическому распределению любого класса, который имеет ваш SSE-вектор в качестве члена, что может быть утомительным, но может быть снова автоматизировано путем получения этих классов из aligned_storage
и помещая весь беспорядок специализации std::allocator
в удобный макрос.
JamesWynn указывает, что эти операции часто объединяются в некоторые специальные тяжелые вычислительные блоки (такие как фильтрация текстур или преобразование вершин), но, с другой стороны, использование этих векторных инкапсуляций SSE не вносит никаких накладных расходов по стандарту float[4]
-выполнение векторного класса. Вам нужно получить эти значения из памяти в регистры в любом случае (будь то стек x87 или скалярный регистр SSE), чтобы делать какие-либо вычисления, поэтому почему бы не взять их все сразу (что должно быть IMHO не медленнее, чем перемещение одного значение, если оно правильно выровнено) и вычислять параллельно. Таким образом, вы можете свободно использовать SSE-дополнение для не SSE без каких-либо накладных расходов (исправьте меня, если мои рассуждения ошибочны).
Но если обеспечение выравнивания для всех классов, имеющих vec4
как члена, слишком утомительно для вас (что является IMHO единственным недостатком этого подхода), вы также можете определить специализированный тип SSE-вектора, который вы используете для вычислений и используйте стандартный вектор без SSE для хранения.
РЕДАКТИРОВАТЬ: Хорошо, чтобы посмотреть на служебный аргумент, который идет здесь (и сначала выглядит вполне разумным), пусть возьмет кучу вычислений, которые выглядят очень чистыми из-за перегруженных операторов
#include "vec.h"
#include <iostream>
int main(int argc, char *argv[])
{
math::vec<float,4> u, v, w = u + v;
u = v + dot(v, w) * w;
v = abs(u-w);
u = 3.0f * w + v;
w = -w * (u+v);
v = min(u, w) + length(u) * w;
std::cout << v << std::endl;
return 0;
}
и посмотрите, что думает об этом VC10:
...
; 6 : math::vec<float,4> u, v, w = u + v;
movaps xmm4, XMMWORD PTR _v$[esp+32]
; 7 : u = v + dot(v, w) * w;
; 8 : v = abs(u-w);
movaps xmm3, XMMWORD PTR [email protected]
movaps xmm1, xmm4
addps xmm1, XMMWORD PTR _u$[esp+32]
movaps xmm0, xmm4
mulps xmm0, xmm1
haddps xmm0, xmm0
haddps xmm0, xmm0
shufps xmm0, xmm0, 0
mulps xmm0, xmm1
addps xmm0, xmm4
subps xmm0, xmm1
movaps xmm2, xmm3
; 9 : u = 3.0f * w + v;
; 10 : w = -w * (u+v);
xorps xmm3, xmm1
andnps xmm2, xmm0
movaps xmm0, XMMWORD PTR [email protected]
mulps xmm0, xmm1
addps xmm0, xmm2
; 11 : v = min(u, w) + length(u) * w;
movaps xmm1, xmm0
mulps xmm1, xmm0
haddps xmm1, xmm1
haddps xmm1, xmm1
sqrtss xmm1, xmm1
addps xmm2, xmm0
mulps xmm3, xmm2
shufps xmm1, xmm1, 0
; 12 : std::cout << v << std::endl;
mov edi, DWORD PTR [email protected]@@[email protected][email protected]@[email protected]@@[email protected]
mulps xmm1, xmm3
minps xmm0, xmm3
addps xmm1, xmm0
movaps XMMWORD PTR _v$[esp+32], xmm1
...
Даже без тщательного анализа каждой отдельной инструкции и ее использования я уверен, что нет никаких лишних загрузок или хранилищ, кроме тех, которые были в начале (Ok, я оставил их неинициализированными), которые необходимы в любом случае, чтобы получить их из памяти в вычислительные регистры, а в конце, что необходимо, как в следующем выражении v
, будет выпущено. Он даже не сохранил ничего в u
и w
, поскольку они являются лишь временными переменными, которые я больше не использую. Все идеально встраивается и оптимизируется. Даже удалось легко перетасовать результат точечного продукта для следующего умножения, не покидая регистр XMM, хотя функция dot
возвращает float
с использованием фактического _mm_store_ss
после haddps
s.
Таким образом, даже я, будучи, как правило, немного превышенным способностью компилятора, должен сказать, что использование собственных встроенных функций в специальных функциях действительно не по сравнению с чистым и выразительным кодом, который вы получаете путем инкапсуляции. Хотя вы можете создавать примеры убийц, где приведение в действие интрий может действительно избавить вас несколькими инструкциями, но затем вам сначала нужно перехитрить оптимизатора.
EDIT:Хорошо, Бен Вейгт указал на еще одну проблему объединения, помимо несовместимости (скорее всего, не проблема) памяти, которая заключается в том, что она нарушает строгие правила псевдонимов, а компилятор может оптимизировать инструкции, обращающиеся к различным членам профсоюза, таким образом, что код недействителен, Я еще об этом не думал. Я не знаю, действительно ли это создает какие-либо проблемы на практике, это, безусловно, требует расследования.
Если это действительно проблема, нам, к сожалению, нужно отбросить член data_[4]
и использовать только __m128
. Для инициализации теперь нужно снова обратиться к _mm_set_ps
и _mm_loadu_ps
. operator[]
становится немного сложнее и может потребоваться некоторая комбинация _mm_shuffle_ps
и _mm_store_ss
. Но для версии non-const вам нужно использовать какой-то прокси-объект, делегирующий присвоение соответствующим инструкциям SSE. Он должен быть исследован, каким образом компилятор может оптимизировать эти дополнительные накладные расходы в определенных ситуациях.
Или вы используете только SSE-вектор для вычислений и просто создаете интерфейс для преобразования в и из не-SSE-векторов в целом, который затем используется в периферийных устройствах вычислений (поскольку вам часто не нужен доступ отдельные компоненты внутри длинных вычислений). Похоже, что glm справляется с этой проблемой. Но я не уверен, как Эйген справляется с этим.
Но как бы вы ни справлялись с этим, все еще нет необходимости в инструментах SSE для ручной работы без использования преимуществ перегрузки оператора.