Для вектора SSE, который имеет все те же компоненты, генерировать на лету или предварительно вычислять?
Когда мне нужно сделать векторную операцию, у которой есть операнд, представляющий собой просто число с плавающей точкой, передаваемое каждому компоненту, я должен предварительно вычислить __m256
или же __m128
и загрузить его, когда мне это нужно, или транслировать число с плавающей точкой в регистр, используя _mm_set1_ps
каждый раз, когда мне нужен вектор?
Я заранее вычислял векторы, которые очень важны и широко используются, и генерировал на лету те, которые менее важны. Но действительно ли я набираю скорость с предкомпьютером? Стоит ли это хлопот?
Это _mm_set1_ps
реализовано с помощью одной инструкции? Это может ответить на мой вопрос.
3 ответа
Естественно, это будет во многом зависеть от вашего кода, но я реализовал две простые функции, используя оба подхода. См код
__m128 calc_set1(float num1, float num2)
{
__m128 num1_4 = _mm_set1_ps(num1);
__m128 num2_4 = _mm_set1_ps(num2);
__m128 result4 = _mm_mul_ps(num1_4, num2_4);
return result4;
}
__m128 calc_mov(float* num1_4_addr, float* num2_4_addr)
{
__m128 num1_4 = _mm_load_ps(num1_4_addr);
__m128 num2_4 = _mm_load_ps(num2_4_addr);
__m128 result4 = _mm_mul_ps(num1_4, num2_4);
return result4;
}
и сборка
calc_set1(float, float):
shufps $0, %xmm0, %xmm0
shufps $0, %xmm1, %xmm1
mulps %xmm1, %xmm0
ret
calc_mov(float*, float*):
movaps (%rdi), %xmm0
mulps (%rsi), %xmm0
ret
Вы можете видеть, что calc_mov()
делает то, что вы ожидаете, и calc_set1()
использует одну инструкцию перемешивания.
movps
инструкция может занять приблизительно четыре цикла для генерации адреса + больше, если порт загрузки кэша L1 занят + больше в редком случае потери кэша.
shufps
займет один цикл на любой из последних микроархитектур Intel. Я считаю, что это правда, будь то для SSE128 или AVX256. Поэтому я бы предложил использовать mm_set1_ps
подход.
Конечно, в инструкции shuffle предполагается, что float уже находится в регистре SSE/AVX. В случае, если вы загружаете его из памяти, трансляция будет лучше, так как она захватит лучшее из movps
а также shufps
в одной инструкции.
Я играл с трансляциями для ответа на самый быстрый способ заполнить вектор (SSE2) определенным значением. Шаблоны дружелюбны. Посмотрите несколько дампов asm трансляций.
set1
каждый раз, когда он используется, не должно иметь большого значения, пока компилятор знает, что значение для широковещания не имеет ничего общего. (Если компилятор не может предположить, что у него нет псевдонима, он должен будет повторять трансляцию после каждой записи в массив или указатель, который может иметь псевдоним.)
Обычно это хороший стиль для хранения set1
результат в именованной переменной. Если у компилятора заканчиваются векторные регистры, он может пролить вектор в стек и перезагрузить его позже, или он может повторно передать. Я не уверен, повлияет ли стиль кодирования на это решение.
Я бы не использовал static const
переменная для кэширования между вызовами функции. (Это может привести к тому, что компилятор генерирует код для проверки, была ли переменная уже инициализирована при каждом вызове.)
Передачи констант времени компиляции иногда приводят к трансляциям времени компиляции, поэтому в вашем коде всего 16B константных данных, находящихся в памяти.
AVX1 передает значение, уже находящееся в регистре, в худшем случае. AVX1 предоставляет только источник памяти vbroadcastps
(использует только порт загрузки). Трансляция занимает shufps / vinsertf128
,
AVX2 требуется для vbroadcastps ymm, xmm
(использует порт случайного воспроизведения)).
Я считаю, что, как правило, лучше выделять вектор SSE из вашего кода (например, цикла) и использовать его всякий раз, когда вам нужно, при условии, что вы позаботитесь о том, чтобы случайно не принудительно вставить его в память. (Например, если вы берете его адрес или передаете его по ссылке на другую функцию, он может быть перенесен в память, и вы можете получить странное поведение.)
Идея состоит в том, что обычно лучше избегать передачи значений в регистры SSE и из них, и если это случится так, что это не так в вашей конкретной ситуации, компилятор уже знает, как было создано значение, и мог бы повторно его преобразовать, если нужно быть. Я думаю, что это намного проще, чем движение кода, инвариантное к циклу, в целом, то есть обратная оптимизация (т. Е. Когда компилятор определяет это для вас) и которая требует, чтобы компилятор доказал, что код действительно инвариантен к циклу.