Встроенные и постоянные переменные 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
).