Интуитивное представление о расположении памяти для быстрого SIMD / проектирования, ориентированного на данные

Недавно я наблюдал за докладами о проектировании, ориентированном на данные, но никогда не понимал причин их единодушного выбора схемы памяти.

Допустим, у нас есть 3D-анимация для рендеринга, и в каждом кадре нам нужно повторно нормализовать наши векторы ориентации.

«Скалярный код»

Они всегда показывают код, который может выглядеть примерно так:

let scene = [{"camera1", vec4{1, 1, 1, 1}}, ...]

for object in scene
    object.orientation = normalize(object.orientation)

Пока все хорошо... Память в &scene может выглядеть примерно так:

[string,X,Y,Z,W,string,X,Y,Z,W,string,X,Y,Z,W,...]

«Код с поддержкой SSE»

Каждое выступление показывает улучшенную, стандартную версию:

let xs = [1, ...]
let ys = [1, ...]
let zs = [1, ...]
let ws = [1, ...]
let scene = [{"camera1", ptr_vec4{&xs[1], &ys[1], &zs[1], &ws[1]}}, ...]

for (o1, o2, o3, o4) in scene
    (o1, o2, o3, o4) = normalize_sse(o1, o2, o3, o4)

Что, благодаря своей структуре памяти, не только более эффективно использует память, но также может обрабатывать 4 объекта сцены одновременно.
Память в &xs, &ys, &zs и &ws

[X,X,X,X,X,X,...]
[Y,Y,Y,Y,Y,Y,...]
[Z,Z,Z,Z,Z,Z,...]
[W,W,W,W,W,W,...]

Но почему 4 отдельных массива?

Если __m128 (packed-4-singles) является преобладающим типом в движках,
я думаю, что это так;
и если тип имеет длину 128 бит,
что, безусловно, так и есть;
и если ширина строки кэша / 128 = 4,
что почти всегда так и есть;
и если x86_64 способна записывать только полную строку кэша,
в чем я почти уверен of
- почему данные не структурированы следующим образом?!

Память на &packed_orientations:

[X,X,X,X,Y,Y,Y,Y,Z,Z,Z,Z,W,W,W,W,X,X,...]
 ^---------cache-line------------^

У меня нет эталона, чтобы протестировать это, и я недостаточно понимаю встроенные функции, чтобы даже попробовать, но, по моей интуиции, не должно ли это быть намного быстрее? Мы бы сократили загрузку и запись страниц в 4 раза, упростили бы выделение памяти и сохранили бы указатели, а код был бы проще, поскольку вместо 4 указателей мы можем добавлять указатели. Я ошибся?

Спасибо! :)


person Community    schedule 04.02.2019    source источник
comment
От того, какие операции вы собираетесь выполнять, зависит эффективность AoS или SoA. Релевантно: 1, 2, и др..   -  person Paul R    schedule 04.02.2019
comment
@PaulR Ну, дизайн, ориентированный на данные, заключается в объединении данных в шаблон последовательного доступа к структурам. На самом деле, если вы хотите получить доступ к каждому компоненту отдельно, это должно стать структурой. Эта гранулярность является своего рода инвариантом к моему вопросу. Но моя идея совсем в другом.   -  person    schedule 04.02.2019
comment
Например, потому что вам может понадобиться выполнять разные операции с разными компонентами. Рассмотрим изображения RGBA: альфа-канал почти всегда обрабатывается иначе, чем каналы r/g/b. Предлагаемый вами макет памяти потребует дополнительного битового сдвига и битового извлечения. Это сложнее кодировать и медленнее выполнять, чем в классической покомпонентной компоновке.   -  person gudok    schedule 04.02.2019
comment
@gudok хорошо, в таком случае, если бы мы строго придерживались шаблона DOD, нам пришлось бы отделить RGB от канала A. Даже если бы мы этого не сделали, нам не потребовалась бы дополнительная работа — шаблон доступа все равно работал бы, но вы определенно потратили бы 75% чтения и записи вашего кэша. Интересно...   -  person    schedule 04.02.2019
comment
@arctiq: gudok уже касался этого, но есть алгоритмы обработки изображений, которые можно применять к пикселю за раз, а есть и другие, где эффективнее обрабатывать одну плоскость за раз. Если вам нужно использовать оба вида, у вас возникает дилемма, использовать ли AoS или SoA. Если вы хотите использовать только один тип шаблона доступа, то выбор относительно прост.   -  person Paul R    schedule 04.02.2019
comment
Этот макет иногда называют AoSoA, потому что он похож на набор небольших кусочков SoA, есть некоторые преимущества, но не 4x.   -  person harold    schedule 04.02.2019
comment
При чем здесь IA64 (Itanium)? SSE — это x86/x86-64 SIMD.   -  person Peter Cordes    schedule 05.02.2019


Ответы (4)


Количество данных, которое вам нужно пройти через вашу подсистему памяти, одинаково, независимо от того, делаете ли вы 4 отдельных массива или предлагаемое вами чередование. Таким образом, вы не сохраняете загрузку или запись страниц (я не понимаю, почему случай «отдельных массивов» должен читать и записывать каждую страницу или строку кэша более одного раза).

Вы больше распределяете передачи памяти - у вас может быть 1 промах кеша L1 на каждой итерации в вашем случае и 4 промаха кеша на каждой 4-й итерации в случае «отдельных массивов». Я не знаю, какой из них был бы предпочтительнее.

В любом случае, главное, чтобы через ваши кеши не проталкивалась ненужная память, с которой вы не взаимодействуете. В вашем примере наличие значений string, которые не читаются и не записываются, но все же проталкиваются через кеши, без необходимости занимает пропускную способность.

person Max Langhof    schedule 04.02.2019
comment
Спасибо, строки в скалярном коде были просто подготовкой к вопросу. Я определенно согласен и понимаю, что удаление неиспользуемых данных необходимо для быстрого кода. Об остальном мне придется подумать. - person ; 04.02.2019
comment
О, конечно, я понял! Все случайно разбросанные массивы будут загружены в L1, а дальше пре-выборщик просто должен отслеживать 4 указателя и стараться не отставать. Вау, это действительно так?! - person ; 04.02.2019
comment
@arctiq: очень сильно зависит от процессора. Высокопроизводительные серверные процессоры, безусловно, способны; дешевый процессор для встраиваемых систем может даже не иметь предварительной выборки. - person MSalters; 04.02.2019
comment
@arctiq Это может быть просто clang глупостью (почему он каждый раз стирает и пересчитывает rax? Я думаю, есть причина, которую я не вижу), но я просто не могу заставить вашу версию с чередованием использовать меньше инструкций, чем версия с отдельными массивами : godbolt.org/z/WehVhR - person Max Langhof; 04.02.2019
comment
В вашем примере Godbolt используются невстроенные вызовы функций, поэтому просмотр генератора кода кажется довольно бесполезным. Возможно, вы искали что-то другое, потому что код, который вы связали, ничего не делает с RAX ни в одной из функций. - person Peter Cordes; 05.02.2019
comment
@PeterCordes Как бы вы написали код для создания соответствующей сборки для сравнения? Я согласен с тем, что код, вероятно, не совсем репрезентативен для реального варианта использования, но я не мог привести его к тому, что подсказывала бы мне моя интуиция (а именно, что увеличение четырех отдельных указателей требует больше инструкций или регистров, чем увеличение одного указателя с четырьмя постоянными смещениями). ). - person Max Langhof; 05.02.2019
comment
@PeterCordes Например, в в этом пункте я просто сдался, потому что (&tomodify - offset) должно быть тривиально для вычисления последовательного rgba4 члены (как только у вас есть первое отличие, остальные должны просто добавлять 128 бит каждый раз), но он использует больше adds. Есть ли какой-то псевдоним, который мне здесь не хватает? - person Max Langhof; 05.02.2019
comment
@MaxLanghof: Наверное, это странно из-за volatile внутри цикла. Ой, подождите, volatile mm128* output; — это указатель на volatile, но сам указатель не изменчив, поэтому для gcc/clang законно оптимизировать цикл, который изменяет его 4 раза до одного хранилища. Это кажется странным; Ассемблер gcc проще (но все же не одно добавление). godbolt.org/z/9_XluC. IDK, я не знаю, почему вы смотрите на циклы, которые возятся только с указателями вместо того, чтобы на самом деле читать память и, например. суммирование значений x или длины вектора суммы квадратов. - person Peter Cordes; 05.02.2019
comment
Ваш вариант 2 очень хорошо компилируется с gcc godbolt.org/z/62L2Yn. 4x movups загружаются с увеличением смещения в режиме адресации, удваивая каждый из них с addps same,same, затем 4x movups сохраняет с тем же режимом адресации. по сравнению с версией с отдельными массивами, увеличивающей 4 указателя. (А при перекрытии проверяется с 2-мя вариантами цикла.) - person Peter Cordes; 05.02.2019
comment
@PeterCordes Я возился только с указателями, а не с памятью, потому что обсуждение было о том, было ли лучше вычисление адресов тем или иным способом (т.е. последующий доступ к памяти в любом случае одинаков), и случай с 4 отдельными указателями должен я конечно потерял его (даже если я испортил volatile). Если я не могу заставить это вести себя так, как ожидалось, то, очевидно, это выше моих сил. И да, эта альтернатива 2 gcc — это то, что я искал. Я думаю, что мораль этой истории - проверьте полученный asm. - person Max Langhof; 05.02.2019

Одним из основных недостатков чередования по ширине вектора является то, что вам нужно изменить макет, чтобы использовать преимущества более широких векторов. (AVX, AVX512).

Но да, когда вы выполняете исключительно ручную векторизацию (без циклов, которые компилятор может автоматически векторизовать с выбором ширины вектора), это может быть того стоит, если все ваши (важные) циклы всегда используют все элементы структуры.

В противном случае применима точка зрения Макса: петля, затрагивающая только x и y, будет тратить пропускную способность на z и w участников.


Однако это не будет намного быстрее; при разумном количестве развертывания цикла индексация 4 массивов или увеличение 4 указателей чуть хуже, чем 1. Предварительная выборка аппаратного обеспечения на процессорах Intel может отслеживать один прямой + 1 обратный поток на страницу размером 4 КБ, поэтому 4 входных потока в основном подходят.

(Но L2 имеет 4-стороннюю ассоциативность в Skylake, по сравнению с 8 ранее, поэтому более 4 входных потоков с одинаковым выравниванием относительно страницы 4k вызовут конфликтные промахи / отказ от предварительной выборки. Таким образом, с более чем 4 большими / page- выровненных массивов, чередующийся формат мог бы избежать этой проблемы.)

Для небольших массивов, где все чередование умещается на одной странице размером 4 КБ, да, это потенциальное преимущество. В противном случае это примерно такое же общее количество затронутых страниц и потенциальных промахов TLB, с одной в 4 раза чаще, чем в группах по 4. Это может быть лучше для предварительной выборки TLB, если вместо этого она может выполнять одну страницу раньше времени. быть заваленным несколькими промахами TLB, происходящими одновременно.


Настройка структуры SoA:

Это может помочь сообщить компилятору, что память, на которую указывает каждый из указателей, не перекрывается. Большинство компиляторов C++ (включая все 4 основных компилятора x86, gcc/clang/MSVC/ICC) поддерживают __restrict в качестве ключевого слова с той же семантикой, что и C99 restrict. Или для переносимости используйте #ifdef / #define, чтобы определить ключевое слово restrict как пустое или __restrict или что-то еще, в зависимости от компилятора.

struct SoA_scene {
        size_t size;
        float *__restrict xs;
        float *__restrict ys;
        float *__restrict zs;
        float *__restrict ws;
};

Это определенно может помочь с автоматической векторизацией, иначе компилятор не будет знать, что xs[i] = foo; не изменяет значение ys[i+1] для следующей итерации.

Если вы читаете эти переменные в локальные переменные (чтобы компилятор был уверен, что присваивание указателя не изменяет сам указатель в структуре), вы можете объявить их как float *__restrict xs = soa.xs; и так далее.

Чередующийся формат по своей сути избегает такой возможности наложения.

person Peter Cordes    schedule 05.02.2019
comment
Заставляет меня задаться вопросом: если вы используете std::vector<float> (без специального распределителя) вместо float* (__restrict), понимает ли компилятор невозможность псевдонимов? - person Max Langhof; 05.02.2019
comment
@MaxLanghof: это может зависеть от качества вашей библиотеки С++. За исключением того, что std::vector не может использовать __restrict внутри, потому что это разобьет код, содержащий float*, на std::vector. Я не думаю, что реализация библиотеки std::vector может что-то сделать с текущими компиляторами, чтобы сообщить ему, что он не является псевдонимом хранилища для других std::vectors того же типа. - person Peter Cordes; 05.02.2019

Одна из еще не упомянутых вещей заключается в том, что доступ к памяти имеет довольно небольшую задержку. И, конечно же, при чтении по 4 указателям результат доступен, когда приходит последнее значение. Таким образом, даже если 3 из 4 значений находятся в кеше, последнее значение может потребоваться из памяти и остановить всю вашу операцию.

Вот почему SSE даже не поддерживает этот режим. Все ваши значения должны быть непрерывными в памяти, и в течение некоторого времени они должны были быть выровнены (чтобы они не могли пересечь границу строки кэша).

Важно отметить, что это означает, что ваш пример (Структура массивов) не работает на оборудовании SSE. Вы не можете использовать элемент [1] из 4 разных векторов в одной операции. Вы можете использовать элементы с [0] по [3] из одного вектора.

person MSalters    schedule 04.02.2019
comment
Смысл кода в том, что вы одновременно захватываете 4 x в mm128, 4 y одновременно и т. д., затем выполняете ту же операцию (которая обычно включает 1 x, 1 y и т. д.) четыре раза одновременно. (Ни в коем случае вы не помещаете x[0], y[0] и т. д. в одно и то же mm128.) Вопрос в том, как наилучшим образом расположить эти группы по 4 в памяти. - person Max Langhof; 04.02.2019
comment
Интересный момент про задержку. Вы неправильно понимаете, каждое поле ориентации находится в другом __mm128, поэтому для сложения: [x1,x2,x3,x4]+[y1,y2,y3,y4]; вместо [x1,y1,z1,w1]+[x2,y2,z2,w2]. Извините, второй пример не имеет смысла ни для какого шага нормализации, но это своего рода приятное последствие макета. - person ; 04.02.2019
comment
@arctiq: Итак, вы говорите, что вызов normalize_sse(o1, o2, o3, o4) на самом деле нормализует 4 массива, а не один 4-вектор? У Макса есть точка выше - это реализация, которая диктует наилучшее расположение памяти. У меня есть какой-то особенно корявый код AVX, где у меня есть частично транспонированная матрица, которая не является ни основной по строкам, ни основной по столбцам. Это имеет смысл только тогда, когда вы выяснили, как используется эта матрица, когда внезапно доступы становятся последовательными. - person MSalters; 04.02.2019

Я реализовал простой тест для обоих методов.

Результат: Полосатый макет в лучшем случае на 10 % быстрее, чем стандартный макет*. Но с SSE4.1 мы можем добиться большего.

*При компиляции с gcc -Ofast на процессоре i5-7200U.

Со структурой немного работать немного проще, но она гораздо менее универсальна. Однако в реальном сценарии это может иметь небольшое преимущество, если распределитель достаточно занят.

Полосатый макет

Time 4624 ms

Memory usage summary: heap total: 713728, heap peak: 713728, stack peak: 2896
         total calls   total memory   failed calls
 malloc|          3         713728              0
realloc|          0              0              0  (nomove:0, dec:0, free:0)
 calloc|          0              0              0
   free|          1         640000
#include <chrono>
#include <cstdio>
#include <random>
#include <vector>
#include <xmmintrin.h>

/* -----------------------------------------------------------------------------
        Striped layout [X,X,X,X,y,y,y,y,Z,Z,Z,Z,w,w,w,w,X,X,X,X...]
----------------------------------------------------------------------------- */

using AoSoA_scene = std::vector<__m128>;

void print_scene(AoSoA_scene const &scene)
{
        // This is likely undefined behavior. Data might need to be stored
        // differently, but this is simpler to index.
        auto &&punned_data = reinterpret_cast<float const *>(scene.data());
        auto scene_size = std::size(scene);

        // Limit to 8 lines
        for(size_t j = 0lu; j < std::min(scene_size, 8lu); ++j) {
                for(size_t i = 0lu; i < 4lu; ++i) {
                        printf("%10.3e ", punned_data[j + 4lu * i]);
                }
                printf("\n");
        }
        if(scene_size > 8lu) {
                printf("(%lu more)...\n", scene_size - 8lu);
        }
        printf("\n");
}

void normalize(AoSoA_scene &scene)
{
        // Euclidean norm, SIMD 4 x 4D-vectors at a time.
        for(size_t i = 0lu; i < scene.size(); i += 4lu) {
                __m128 xs = scene[i + 0lu];
                __m128 ys = scene[i + 1lu];
                __m128 zs = scene[i + 2lu];
                __m128 ws = scene[i + 3lu];

                __m128 xxs = _mm_mul_ps(xs, xs);
                __m128 yys = _mm_mul_ps(ys, ys);
                __m128 zzs = _mm_mul_ps(zs, zs);
                __m128 wws = _mm_mul_ps(ws, ws);

                __m128 xx_yys = _mm_add_ps(xxs, yys);
                __m128 zz_wws = _mm_add_ps(zzs, wws);

                __m128 xx_yy_zz_wws = _mm_add_ps(xx_yys, zz_wws);

                __m128 norms = _mm_sqrt_ps(xx_yy_zz_wws);

                scene[i + 0lu] = _mm_div_ps(xs, norms);
                scene[i + 1lu] = _mm_div_ps(ys, norms);
                scene[i + 2lu] = _mm_div_ps(zs, norms);
                scene[i + 3lu] = _mm_div_ps(ws, norms);
        }
}

float randf()
{
        std::random_device random_device;
        std::default_random_engine random_engine{random_device()};
        std::uniform_real_distribution<float> distribution(-10.0f, 10.0f);
        return distribution(random_engine);
}

int main()
{
        // Scene description, e.g. cameras, or particles, or boids etc.
        // Has to be a multiple of 4!   -- No edge case handling.
        std::vector<__m128> scene(40'000);

        for(size_t i = 0lu; i < std::size(scene); ++i) {
                scene[i] = _mm_set_ps(randf(), randf(), randf(), randf());
        }

        // Print, normalize 100'000 times, print again

        // Compiler is hopefully not smart enough to realize
        // idempotence of normalization
        using std::chrono::steady_clock;
        using std::chrono::duration_cast;
        using std::chrono::milliseconds;
        // >:(

        print_scene(scene);
        printf("Working...\n");

        auto begin = steady_clock::now();
        for(int j = 0; j < 100'000; ++j) {
                normalize(scene);
        }
        auto end = steady_clock::now();
        auto duration = duration_cast<milliseconds>(end - begin);

        printf("Time %lu ms\n", duration.count());
        print_scene(scene);

        return 0;
}

Компоновка SoA

Time 4982 ms

Memory usage summary: heap total: 713728, heap peak: 713728, stack peak: 2992
         total calls   total memory   failed calls
 malloc|          6         713728              0
realloc|          0              0              0  (nomove:0, dec:0, free:0)
 calloc|          0              0              0
   free|          4         640000
#include <chrono>
#include <cstdio>
#include <random>
#include <vector>
#include <xmmintrin.h>

/* -----------------------------------------------------------------------------
        SoA layout [X,X,X,X,...], [y,y,y,y,...], [Z,Z,Z,Z,...], ...
----------------------------------------------------------------------------- */

struct SoA_scene {
        size_t size;
        float *xs;
        float *ys;
        float *zs;
        float *ws;
};

void print_scene(SoA_scene const &scene)
{
        // This is likely undefined behavior. Data might need to be stored
        // differently, but this is simpler to index.

        // Limit to 8 lines
        for(size_t j = 0lu; j < std::min(scene.size, 8lu); ++j) {
                printf("%10.3e ", scene.xs[j]);
                printf("%10.3e ", scene.ys[j]);
                printf("%10.3e ", scene.zs[j]);
                printf("%10.3e ", scene.ws[j]);
                printf("\n");
        }
        if(scene.size > 8lu) {
                printf("(%lu more)...\n", scene.size - 8lu);
        }
        printf("\n");
}

void normalize(SoA_scene &scene)
{
        // Euclidean norm, SIMD 4 x 4D-vectors at a time.
        for(size_t i = 0lu; i < scene.size; i += 4lu) {
                __m128 xs = _mm_load_ps(&scene.xs[i]);
                __m128 ys = _mm_load_ps(&scene.ys[i]);
                __m128 zs = _mm_load_ps(&scene.zs[i]);
                __m128 ws = _mm_load_ps(&scene.ws[i]);

                __m128 xxs = _mm_mul_ps(xs, xs);
                __m128 yys = _mm_mul_ps(ys, ys);
                __m128 zzs = _mm_mul_ps(zs, zs);
                __m128 wws = _mm_mul_ps(ws, ws);

                __m128 xx_yys = _mm_add_ps(xxs, yys);
                __m128 zz_wws = _mm_add_ps(zzs, wws);

                __m128 xx_yy_zz_wws = _mm_add_ps(xx_yys, zz_wws);

                __m128 norms = _mm_sqrt_ps(xx_yy_zz_wws);

                __m128 normed_xs = _mm_div_ps(xs, norms);
                __m128 normed_ys = _mm_div_ps(ys, norms);
                __m128 normed_zs = _mm_div_ps(zs, norms);
                __m128 normed_ws = _mm_div_ps(ws, norms);

                _mm_store_ps(&scene.xs[i], normed_xs);
                _mm_store_ps(&scene.ys[i], normed_ys);
                _mm_store_ps(&scene.zs[i], normed_zs);
                _mm_store_ps(&scene.ws[i], normed_ws);
        }
}

float randf()
{
        std::random_device random_device;
        std::default_random_engine random_engine{random_device()};
        std::uniform_real_distribution<float> distribution(-10.0f, 10.0f);
        return distribution(random_engine);
}

int main()
{
        // Scene description, e.g. cameras, or particles, or boids etc.
        // Has to be a multiple of 4!   -- No edge case handling.
        auto scene_size = 40'000lu;
        std::vector<float> xs(scene_size);
        std::vector<float> ys(scene_size);
        std::vector<float> zs(scene_size);
        std::vector<float> ws(scene_size);

        for(size_t i = 0lu; i < scene_size; ++i) {
                xs[i] = randf();
                ys[i] = randf();
                zs[i] = randf();
                ws[i] = randf();
        }

        SoA_scene scene{
                scene_size,
                std::data(xs),
                std::data(ys),
                std::data(zs),
                std::data(ws)
        };
        // Print, normalize 100'000 times, print again

        // Compiler is hopefully not smart enough to realize
        // idempotence of normalization
        using std::chrono::steady_clock;
        using std::chrono::duration_cast;
        using std::chrono::milliseconds;
        // >:(

        print_scene(scene);
        printf("Working...\n");

        auto begin = steady_clock::now();
        for(int j = 0; j < 100'000; ++j) {
                normalize(scene);
        }
        auto end = steady_clock::now();
        auto duration = duration_cast<milliseconds>(end - begin);

        printf("Time %lu ms\n", duration.count());
        print_scene(scene);

        return 0;
}

Макет AoS

Начиная с SSE4.1, кажется, есть третий вариант — безусловно, самый простой и быстрый.

Time 3074 ms

Memory usage summary: heap total: 746552, heap peak: 713736, stack peak: 2720
         total calls   total memory   failed calls
 malloc|          5         746552              0
realloc|          0              0              0  (nomove:0, dec:0, free:0)
 calloc|          0              0              0
   free|          2         672816
Histogram for block sizes:
    0-15              1  20% =========================
 1024-1039            1  20% =========================
32816-32831           1  20% =========================
   large              2  40% ==================================================

/* -----------------------------------------------------------------------------
        AoS layout [{X,y,Z,w},{X,y,Z,w},{X,y,Z,w},{X,y,Z,w},...]
----------------------------------------------------------------------------- */

using AoS_scene = std::vector<__m128>;

void print_scene(AoS_scene const &scene)
{
        // This is likely undefined behavior. Data might need to be stored
        // differently, but this is simpler to index.
        auto &&punned_data = reinterpret_cast<float const *>(scene.data());
        auto scene_size = std::size(scene);

        // Limit to 8 lines
        for(size_t j = 0lu; j < std::min(scene_size, 8lu); ++j) {
                for(size_t i = 0lu; i < 4lu; ++i) {
                        printf("%10.3e ", punned_data[j * 4lu + i]);
                }
                printf("\n");
        }
        if(scene_size > 8lu) {
                printf("(%lu more)...\n", scene_size - 8lu);
        }
        printf("\n");
}

void normalize(AoS_scene &scene)
{
        // Euclidean norm, SIMD 4 x 4D-vectors at a time.
        for(size_t i = 0lu; i < scene.size(); i += 4lu) {
                __m128 vec = scene[i];
                __m128 dot = _mm_dp_ps(vec, vec, 255);
                __m128 norms = _mm_sqrt_ps(dot);
                scene[i] = _mm_div_ps(vec, norms);
        }
}

float randf()
{
        std::random_device random_device;
        std::default_random_engine random_engine{random_device()};
        std::uniform_real_distribution<float> distribution(-10.0f, 10.0f);
        return distribution(random_engine);
}

int main()
{
        // Scene description, e.g. cameras, or particles, or boids etc.
        std::vector<__m128> scene(40'000);

        for(size_t i = 0lu; i < std::size(scene); ++i) {
                scene[i] = _mm_set_ps(randf(), randf(), randf(), randf());
        }

        // Print, normalize 100'000 times, print again

        // Compiler is hopefully not smart enough to realize
        // idempotence of normalization
        using std::chrono::steady_clock;
        using std::chrono::duration_cast;
        using std::chrono::milliseconds;
        // >:(

        print_scene(scene);
        printf("Working...\n");

        auto begin = steady_clock::now();
        for(int j = 0; j < 100'000; ++j) {
                normalize(scene);
                //break;
        }
        auto end = steady_clock::now();
        auto duration = duration_cast<milliseconds>(end - begin);

        printf("Time %lu ms\n", duration.count());
        print_scene(scene);

        return 0;
}
person Community    schedule 04.02.2019
comment
Одним из основных недостатков чередования по ширине вектора является то, что вам нужно изменить макет, чтобы воспользоваться преимуществами более широких векторов. (AVX, AVX512). Но да, когда вы вручную векторизуете, это может быть оправдано, если все ваши (важные) циклы всегда используют все члены структуры. В противном случае применима точка зрения Макса: петля, затрагивающая только x и y, будет тратить пропускную способность на z и w участников. - person Peter Cordes; 05.02.2019
comment
*(scene.xs + j). C++ имеет для этого более простой синтаксис: scene.xs[j]. - person Peter Cordes; 05.02.2019
comment
Я часто пишу такие вещи, как _mm_load_ps(scene.xs + i), а не &scene.xs[i], но оба варианта допустимы. (Конечно, в коде, который я пишу сам, я склонен использовать приращение указателя в C++, потому что это обычно то, что я хочу в ассемблере. Процессоры Intel могут поддерживать индексированные режимы адресации только в ограниченных случаях, не включая VEX. инструкции load+ALU. режимы микрослияния и адресации) - person Peter Cordes; 06.02.2019
comment
@PeterCordes, поскольку переменные являются указателями, мне кажется непоследовательным притворяться, что они являются массивами в одном месте, а затем переключаться обратно на арифметику указателей в другом. Подумай о новичках, чувак! С++ и так достаточно запутан. :) - person ; 06.02.2019
comment
Я обычно пишу в основном C-подобный код при оптимизации. Как я уже сказал, я обычно пишу код, который увеличивает указатель, не притворяясь массивом. Если вы пытаетесь сделать свой код похожим на массивы, то обязательно используйте &arr[i], но мне это кажется шумным по сравнению с arr + i. Если выражение включает доступ к памяти, то у меня квадратные скобки. Как _mm_set1_ps(arr[i]). Если это не так (просто вычисляю указатель, даже если я передаю его в загрузку или храню встроенный), то никаких скобок. То же, что и asm с синтаксисом NASM (ошибка, если вы игнорируете LEA :P). - person Peter Cordes; 06.02.2019