Постоянная нагрузки плавает в регистры SSE
Я пытаюсь найти эффективный способ загрузки поплавков постоянной времени компиляции в регистры SSE (2/3). Я пробовал делать простой код, например,
const __m128 x = { 1.0f, 2.0f, 3.0f, 4.0f };
но который генерирует 4 команды movss из памяти!
movss xmm0,dword ptr [[email protected] (14048E534h)]
movss xmm1,dword ptr [[email protected] (14048E530h)]
movaps xmm6,xmm12
shufps xmm6,xmm12,0C6h
movss dword ptr [rsp],xmm0
movss xmm0,dword ptr [[email protected] (14048E52Ch)]
movss dword ptr [rsp+4],xmm1
movss xmm1,dword ptr [[email protected] (14048E528h)]
которые загружают скаляры в и из памяти... (?!?!)
Выполнение этого, хотя..
float Align(16) myfloat4[4] = { 1.0f, 2.0f, 3.0f, 4.0f, }; // out in global scope
генерирует.
movaps xmm5,xmmword ptr [::myarray4 (140512050h)]
В идеале было бы неплохо, если бы у меня были константы, то это было бы способом не касаться памяти и просто делать это с помощью непосредственных инструкций стиля (например, константы, скомпилированные в самой инструкции).
Спасибо
Ответы
Ответ 1
Если вы хотите принудительно установить его на одну нагрузку, вы можете попробовать (gcc):
__attribute__((aligned(16))) float vec[4] = { 1.0f, 1.1f, 1.2f, 1.3f };
__m128 v = _mm_load_ps(vec); // edit by sor: removed the "&" cause its already an address
Если у вас есть Visual С++, используйте __declspec(align(16))
для запроса правильного ограничения.
В моей системе это (скомпилировано с gcc -m32 -msse -O2
; отсутствие оптимизации вообще загромождает код, но в конце концов сохраняет единственный movaps
), создает следующий код сборки (синтаксис gcc/AT & T):
andl $-16, %esp
subl $16, %esp
movl $0x3f800000, (%esp)
movl $0x3f8ccccd, 4(%esp)
movl $0x3f99999a, 8(%esp)
movl $0x3fa66666, 12(%esp)
movaps (%esp), %xmm0
Обратите внимание, что он выравнивает указатель стека перед распределением стекового пространства и помещением констант в него. Выход из __attribute__((aligned))
out может, в зависимости от вашего компилятора, создать неправильный код, который этого не сделает, поэтому будьте осторожны и проверьте разборку.
Дополнительно:
Поскольку вы запрашиваете, как вставлять константы в код, просто попробуйте выше с помощью static
квалификатора для массива float
. Это создает следующую сборку:
movaps vec.7330, %xmm0
...
vec.7330:
.long 1065353216
.long 1066192077
.long 1067030938
.long 1067869798
Ответ 2
Во-первых, какой уровень оптимизации вы компилируете? Это не редкость видеть, что вид codegen на -O0 или -O1, но я был бы очень удивлен, увидев его с -O2 или выше в большинстве компиляторов.
Во-вторых, в SSE нет непосредственных нагрузок. Вы можете немедленно загрузить GPR, а затем перенести это значение в SSE, но не можете вызвать другие значения без фактической нагрузки (игнорируя некоторые специальные значения, такие как 0
или (int)-1
, которые могут быть созданы с помощью логических операций.
Наконец, если генерируется плохой код с включенными оптимизациями и в критическом для производительности месте, пожалуйста, сообщите об ошибке в отношении вашего компилятора.
Ответ 3
Обычно такие константы будут загружаться до любых циклов или "горячих" частей кода, поэтому производительность не должна быть такой важной. Но если вы не можете избежать такого рода вещей внутри цикла, я сначала попробую _mm_set_ps
и посмотрю, что это генерирует. Также попробуйте ICC, а не gcc, поскольку он имеет тенденцию генерировать лучший код.
Ответ 4
Генерирующие константы намного проще (и быстрее), если четыре константы поплавка одинаковы. Например, битовая диаграмма для 1.f равна 0x3f800000. Один из способов можно создать с помощью SSE2
register __m128i onef;
__asm__ ( "pcmpeqb %0, %0" : "=x" ( onef ) );
onef = _mm_slli_epi32( onef, 25 );
onef = _mm_srli_epi32( onef, 2 );
Другой подход с SSE4.1:
register uint32_t t = 0x3f800000;
register __m128 onef;
__asm__ ( "pinsrd %0, %1, 0" : "=x" ( onef ) : "r" ( t ) );
onef = _mm_shuffle_epi32( onef, 0 );
Обратите внимание, что я не могущественный, если эта версия работает быстрее, чем SSE2, не профилировала ее, проверяла только результат.
Если значения каждого из четырех поплавков должны быть разными, тогда каждая из констант может быть сгенерирована и перетасована или смешана вместе.
Wether или нет, это полезно, зависит от того, вероятна ли ошибка кеша, иначе загрузка константы из памяти происходит быстрее. Хитрости, подобные этому, очень полезны в vmx/altivec, но большие кеши на большинстве ПК могут сделать это менее полезным для sse.
Это хорошее обсуждение этого в Руководстве по оптимизации Agner Fog, книга 2, раздел 13.4, http://www.agner.org/optimize/.
Заключительное примечание: использование встроенного ассемблера выше - спецификация gcc, причина заключается в том, чтобы разрешить использование неинициализированных переменных без генерации предупреждения компилятора. С помощью vc вам может потребоваться или не понадобиться сначала инициализировать переменные с помощью _mm_setzero_ps(), а затем надеяться, что оптимизатор может удалить это.