Встроенные и постоянные переменные SIMD / состояние

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

поэтому главный вопрос, который у меня есть, - это использование встроенных типов данных SIMD, таких как __m256. И чтобы перейти к сути, мой вопрос заключается в том, чтобы делать такие вещи:

class PersistentObject
{
     ...
private:
     std::vector<__m256, AlignedAlloc<__m256, 32>> data;
};

это грубо, приемлемо, будет ли это срабатывать компиляторы, когда дело доходит до создания наиболее эффективного кода? Вот что меня сейчас смущает. Я на неопытном уровне, когда у меня есть точка доступа и исчерпаны все другие немедленные варианты, я даю SIMD внутреннеприсущему выстрел и всегда ищу, чтобы отказаться от моих изменений, если они не улучшают производительность (и я отступил так много изменений, связанных с SIMD).

но этот вопрос и путаница у меня есть о хранении SIMD внутренних типов настойчиво также заставили меня поймите, что я действительно не понимаю, как эти внутренние компоненты работают на фундаментальном уровне компилятора. Мой разум хочет думать о __m256 как абстрактный YMM регистрация (не обязательно выделенный). Это начинает щелкать со мной, когда я вижу инструкции по загрузке и хранению. Я думаю о них как о подсказках для компилятора для выполнения его распределения регистра.

и мне не нужно было думать об этом больше, чем раньше, потому что я всегда использовал типы SIMD временным способом: _mm256_load_ps к __m256, выполните некоторые операции, сохраните результаты обратно в 32-битный spfp 256-битный выровненный массив float[8]. Мне сошло с рук думать о __m256 как регистр YMM.

абстрактный регистр YMM?

но недавно я реализовывал структуру данных, которая пытается вращаться вокруг обработки SIMD (простой, представляющий кучу векторов в SoA-моде), и здесь становится удобно, если я могу просто работать преимущественно с __m256 без постоянно загрузка из массива поплавков и сохранение результатов после. И в некоторых быстрых тестах MSVC, по крайней мере, кажется, испускает соответствующие инструкции, отображающие мои внутренние компоненты в сборку (наряду с правильными выровненными нагрузками и хранилищами, когда я получаю доступ к данным из вектора). Но это нарушает мою концептуальную модель мышления __m256 как абстрактное YMM register, потому что хранение этих вещей постоянно подразумевает что-то вроде регулярной переменной, но в этот момент что происходит с нагрузками / movs а магазины?

поэтому я немного спотыкаюсь о концептуальную модель, которую я построил в своей голове о том, как думать обо всем этом, и я надеюсь, что, возможно, кто-то опытный может сразу распознать, что сломано с тем, как я думаю об этом материале, и дать мне тот ответ Эврика, который отлаживает мой мозг. Я надеюсь, что этот вопрос не слишком глуп (у меня есть неприятное чувство, что это так, но я пытался найти ответ в другом месте, только чтобы все еще запутаться). Так что, в конечном счете, допустимо напрямую хранить эти типы данных постоянно (подразумевая, что мы перезагрузим память в какой-то момент после того, как она уже выплеснулась из регистра YMM без использования _mm_load*), и если да, то что не так с моей концептуальной моделью?

извинения, если это глупый вопрос! Я действительно промокла насквозь от этой дряни.

Подробнее

большое спасибо за полезные комментарии до сих пор! Наверное, мне стоит поделиться. еще несколько деталей, чтобы сделать мой вопрос менее расплывчато. В основном я пытаюсь создать структуру данных, которая немного больше, чем коллекция векторов, хранящихся в форме SoA:

xxxxxxxx....
yyyyyyyy....
zzzzzzzz....

... и главным образом с намерением быть использованным для горячих точек где критические петли имеют последовательную картину доступа. Но в то же время некритические пути выполнения могут захотеть случайным образом получить доступ к 5-му 3-вектору в форме AoS (x/y/z), и в этот момент мы неизбежно делаем скалярный доступ (который прекрасно, если это не так эффективно, поскольку они не являются критическими путями).

в этом одном специфическом случае мне было бы намного удобнее с точки зрения реализации просто настойчиво хранить и работать с __m256 вместо float*. Это помешало бы мне посыпать много вертикального петлевого кода с _mm_loads* и _mm_stores* потому что общий случай в этом сценарии (как с точки зрения критического выполнения, так и основной части кода) реализуется с помощью встроенных SIMD. Но я не уверен, что это разумная практика над просто резервированием __m256 для кратковременных временных данных, локальных для некоторой функции, чтобы загрузить некоторые поплавки в __m256, выполнить некоторые операции и сохранить результаты, как я обычно делал в прошлом. Это было бы немного удобнее, но я немного обеспокоен тем, что этот удобный тип реализации может задушить некоторые оптимизаторы (хотя я еще не нашел, что это так). И если они не спотыкаются об оптимизаторы, то как я был думать об этих типах данных было немного не так все это время.

Итак, в этом случае, это похоже на то, что если это прекрасно делать, и наши оптимизаторы справляются с этим блестяще все время, то я смущен, потому что то, как я думал об этом материале и думал, что нам нужны эти явные _mm_load и _mm_store в кратковременных контекстах (локальных для функции, т. е.), чтобы помочь нашим оптимизаторам, все было неправильно! И это расстраивает меня, что это работает нормально, потому что я этого не делал думаю, это должно было сработать! :- D

ответы

если это поможет, у меня есть около 200k LOC написано именно так. IOW, I относитесь к типу SIMD как к первоклассному гражданину. Все нормально. Компилятор ручками их не иначе, чем любой другой примитивный тип. Так есть нет никаких проблем с этим.

оптимизаторы не настолько хлипкие. Они сохраняют верность в разумные интерпретации стандартов C / C++. Нагрузка / магазин внутренние компоненты действительно не нужны, если вам не нужны специальные (не выровненный, не темпоральный, в маске, так далее...)

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

Отражает

еще раз большое спасибо всем! Теперь я чувствую себя намного яснее и увереннее. проектирование кода, построенного вокруг SIMD. По какой-то причине я был чрезвычайно подозрителен к оптимизатору только для встроенных SIMD, думая, что мне нужно написать свой код на самом низком уровне и иметь эти нагрузки и магазины как можно более локальными в ограниченной области функций. Я думаю, что некоторые из моих суеверий произошли от написания SIMD intrinsics первоначально против старых компиляторов почти пару десятилетий назад, и, возможно, тогда оптимизаторам, возможно, нужна была дополнительная помощь, или, может быть, я просто был иррационально суеверен все это время. Я смотрел на это так, как люди смотрели на компиляторы C в 80-х годах, помещая такие вещи, как register намеки здесь и там.

1 ответов


да __m256 работает как обычный тип; он не должен быть только для регистрации. Вы можете сделать массивы __m256, передайте их по ссылке на не встроенные функции и все остальное.

основное предостережение заключается в том, что это" выровненный " тип: компилятор предполагает, что __m256 в памяти 32 байта выровнены, но std::max_align_t обычно имеет только выравнивание 8 или 16 байтов на основных реализациях C++. Таким образом, вам нужен этот пользовательский распределитель для std::vector или другие динамические распределения, потому что std::vector<__m256> выделит память, которая недостаточно выровнена для хранения __m256. Спасибо, C++ (хотя C++17, по-видимому, наконец исправит это).


но это нарушает мою концептуальную модель мышления __m256 как абстрактный регистр YMM, потому что хранение этих вещей постоянно подразумевает что-то вроде регулярной переменной, но в этот момент что происходит с нагрузками/movs и магазинами?

на __m128 _mm_loadu_ps(float*) / _mm_load_ps intrinsics в основном существуют для передачи информации о выравнивании компилятору и (для FP intrinsics) для приведения типа. С integer you они даже не делают этого, и вы должны бросать указатели на __m128i*.

(AVX512 встроенные функции, наконец, использовать void* вместо __m512i*, хотя.)

_mm256_load_ps(fp) в основном эквивалентно *(__m256*)fp: выровненная нагрузка 8 поплавков. __m256* допускается псевдоним других типов, но (как я понимаю) обратное не true: не гарантируется безопасность получения 3-го элемента __m256 my_vec с кодом ((float*)my_vec)[3]. Что будет строгий сглаживания нарушение. Хотя на практике он работает, по крайней мере, большую часть времени на большинстве компиляторов.

(см. получить член __m128 по индексу?, а также печать переменной __m128i для портативного способа: хранение в массив tmp часто оптимизируется. Но если вы хотите горизонтальную сумму или что-то еще, это обычно лучше всего использовать вектор shuffle и добавить встроенные, вместо того, чтобы надеяться, что компилятор автоматически векторизует цикл store + scalar add.)


может быть, в какой-то момент в прошлом, когда внутренние компоненты были новыми, вы действительно получили movaps загрузить каждый раз, когда ваш источник C содержал _mm_load_ps, но на данный момент это не особо отличаются от * оператора float*; компилятор может и будет оптимизировать избыточные нагрузки одних и тех же данных или оптимизировать вектор магазин / скалярная перезагрузка в перетасовку.


но в то же время некритические пути выполнения могут захотеть случайным образом получить доступ к 5-му 3-вектору в форме AoS (x/y/z), и в этот момент мы неизбежно делаем скалярный доступ.

самое большое предостережение здесь заключается в том, что код для получения скаляров из __m256 объекты будут уродливыми и, возможно, не будут компилироваться эффективно. Вы можете скрыть уродство с помощью функций обертки, но проблемы с эффективностью может не уйти легко, в зависимости от вашего компилятора.

если вы пишете портативный код, который не использует GCC-style my_vec[3] или MSVC my_vec.m256_f32[3] хранение __m256 к массиву, как alignas(32) float tmp [8] может не оптимизироваться, и вы можете получить нагрузку в регистр YMM и магазин. (И затем vzeroupper).