Как пользоваться 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;
}

Сейчас функция с векторизацией в два раза медленней обычной.

  1. Легально ли напрямую копировать память из _m128, или обязательно использовать _mm_storeu_ps() ?
  2. Легально ли засовывать в _m128 мусор (структура весит 8 байт, читаются 8 лишних за ней)
  3. Как загрузить в _m128 только два float числа? Можно ли загружать в _m128 напрямую через memcpy?
  4. Как выжать максимум скорости? Есть ли смысл использовать векторизацию для всего двух чисел? (компилятор скорее всего сам это сделал, но просто интересно, возможно ли хотя бы приблизиться к результатам 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 шт):

Автор решения: Ariox

Вопросы 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++, но не смог оптимизировать функцию с ручной оптимизацией.

→ Ссылка