Как пользоваться simd инструкциями в c++?
у меня есть простая структура-вектор
struct v2f
{
float x;
float y;
};
есть две функции для работы с ними
v2f addNormal(const v2f& v1, const v2f& v2) {
v2f vec;
vec.x = v1.x + v2.x;
vec.y = v1.y + v2.y;
return vec;
}
v2f addVectorized(const v2f& v1, const v2f& v2) {
v2f vec;
__m128 res = _mm_add_ps(
_mm_loadu_ps((float*)(&v1)),
_mm_loadu_ps((float*)(&v2))
);
memcpy(&vec, res.m128_f32, sizeof(float) * 2);
return vec;
}
Сейчас функция с векторизацией в два раза медленней обычной.
- Легально ли напрямую копировать память из _m128, или обязательно использовать _mm_storeu_ps() ?
- Легально ли засовывать в _m128 мусор (структура весит 8 байт, читаются 8 лишних за ней)
- Как загрузить в _m128 только два float числа? Можно ли загружать в _m128 напрямую через memcpy?
- Как выжать максимум скорости? Есть ли смысл использовать векторизацию для всего двух чисел? (компилятор скорее всего сам это сделал, но просто интересно, возможно ли хотя бы приблизиться к результатам 1-й функции)
Вот новый код
v2f addVectorized(const v2f& v1, const v2f& v2) {
v2f vec;
const __m128 zero = _mm_setzero_ps();
const __m128 res = _mm_add_ps(
_mm_loadh_pi(zero, (__m64*)(&v1)),
_mm_loadh_pi(zero, (__m64*)(&v2))
);
_mm_storeh_pi((__m64*)(&vec), res);
return vec;
}
Теперь он работает в 1.5 раза медленней обычного
Как его можно ещё оптимизировать?
Что примечательно, при добавлении в структуру полей z,w и работы с 4 значениями, а не с двумя, sse код выигрывает обычный (примерно в 2 раза)
Ответы (1 шт):
Вопросы 1-3 - это все неопределенное поведение. Ваша исходная реализация - тоже, т.к. происходит обращение к невыделенной области памяти. Т.е. это может работать, может не работать, и смысла так делать никакого, т.к. либо вы доверяете оптимизацию компилятору, либо делаете согласно документации и стандарту, и надеетесь, что реализация тоже работает согласно документации. Например, вы не можете записать в память два значения float, но вы можете записать четыре, два из которых не используются. Два лишних значения в любом случае должны откуда-то взяться, и нужно указать, откуда.
Тут стоит уточнить, что вышесказанное относится конкретно к C++. На OpenCL, например, правила несколько другие. На чистом C тоже допустимы вольности, но обращений к невыделенной памяти и там быть не должно.
Ответ на 4: Чтобы ваши попытки оптимизации хоть как-то работали, структура должна быть выровнена в памяти. Посмотрите документацию, большая часть функций требует выравнивания 16 байт, если выравнивания нет - программа может вылететь, либо компилятор добавит выравнивание ценой производительности.
Более-менее правильный подход выглядит так:
#include<xmmintrin.h>
#include<cstring>
struct alignas(16) v2f // по хорошему. нужно вообще использовать __attribute__((packed, aligned(16)))
{
float x;
float y;
};
v2f addNormal(const v2f& v1, const v2f& v2) {
return {v1.x + v2.x, v1.y + v2.y};
}
v2f addVectorized(const v2f& v1, const v2f& v2) {
v2f v1_vec[2] = {v1, v1};
v2f v2_vec[2] = {v2, v2};
const __m128 v1_ = _mm_load_ps(&v1_vec[0].x);
const __m128 v2_ = _mm_load_ps(&v2_vec[0].x);
const __m128 res = _mm_add_ps(
v1_,
v2_
);
v2f res_v[2];
_mm_store_ps (&res_v[0].x, res);
return res_v[0];
}
Ассемблер для gcc (https://godbolt.org/z/7b86Kv5js):
addNormal(v2f const&, v2f const&):
movq xmm0, QWORD PTR [rdi]
movq xmm1, QWORD PTR [rsi]
addps xmm0, xmm1
ret
addVectorized(v2f const&, v2f const&):
movaps xmm0, XMMWORD PTR [rsi]
addps xmm0, XMMWORD PTR [rdi]
ret
Результат не эквивалентен, но по крайней мере нет лишних перемещений между регистрами, просто разный способ инициализации регистра. Все лишнее опять же оптимизировано компилятором, и видно, что компилятор сам использует векторизацию для двух чисел, т.е. это нормально.
Но даже в корректной реализации есть проблема:
v2f test1(){
v2f v1{1.1, 3.1};
v2f v2{42.1 ,1};
return addNormal(v1, v2);
}
v2f test2(){
v2f v1{1, 3};
v2f v2{3 ,1};
return addVectorized(v1, v2);
}
test1():
movq xmm0, QWORD PTR .LC0[rip]
ret
test2():
mov QWORD PTR [rsp-32], 0
mov rax, QWORD PTR .LC1[rip]
mov QWORD PTR [rsp-16], 0
mov QWORD PTR [rsp-40], rax
mov rax, QWORD PTR .LC2[rip]
movaps xmm0, XMMWORD PTR [rsp-40]
mov QWORD PTR [rsp-24], rax
addps xmm0, XMMWORD PTR [rsp-24]
ret
.LC0:
.long 1110232268
.long 1082340147
.LC1:
.long 1066192077
.long 1078355558
.LC2:
.long 1109943910
.long 1065353216
Компилятор полностью оптимизировал функцию, написанную на стандартном c++, но не смог оптимизировать функцию с ручной оптимизацией.