Наказание за переход с SSE на AVX?

Мне известно о существующем наказании за переход от инструкций AVX к инструкциям SSE без предварительного обнуления верхних половин всех регистров ymm, но в моем конкретном случае на моей машине (i7-3939K 3,2 ГГц), похоже, очень большое наказание за обход (SSE к AVX), даже если я явно использую _mm256_zeroupper до и после раздела кода AVX.

Я написал функции для преобразования между 32-битными числами с плавающей точкой и 32-битными целыми числами с фиксированной запятой на 2 буфера, которые имеют ширину 32768 элементов. Я портировал встроенную версию SSE2 непосредственно в AVX, чтобы одновременно выполнять 8 элементов по сравнению с 4 SSE, ожидая значительного увеличения производительности, но, к сожалению, произошло обратное.

Итак, у меня есть 2 функции:

void ConvertPcm32FloatToPcm32Fixed(int32* outBuffer, const float* inBuffer, uint sampleCount, bool bUseAvx)
{
const float fScale = (float)(1U<<31);

if (bUseAvx)
{
_mm256_zeroupper();
const __m256 vScale = _mm256_set1_ps(fScale);
const __m256 vVolMax = _mm256_set1_ps(fScale-1);
const __m256 vVolMin = _mm256_set1_ps(-fScale);

for (uint i = 0; i < sampleCount; i+=8)
{
const __m256 vIn0 = _mm256_load_ps(inBuffer+i); // Aligned load
const __m256 vVal0 = _mm256_mul_ps(vIn0, vScale);
const __m256 vClamped0 = _mm256_min_ps( _mm256_max_ps(vVal0, vVolMin), vVolMax );
const __m256i vFinal0 = _mm256_cvtps_epi32(vClamped0);
_mm256_store_si256((__m256i*)(outBuffer+i), vFinal0); // Aligned store
}
_mm256_zeroupper();
}
else
{
const __m128 vScale = _mm_set1_ps(fScale);
const __m128 vVolMax = _mm_set1_ps(fScale-1);
const __m128 vVolMin = _mm_set1_ps(-fScale);

for (uint i = 0; i < sampleCount; i+=4)
{
const __m128 vIn0 = _mm_load_ps(inBuffer+i); // Aligned load
const __m128 vVal0 = _mm_mul_ps(vIn0, vScale);
const __m128 vClamped0 = _mm_min_ps( _mm_max_ps(vVal0, vVolMin), vVolMax );
const __m128i vFinal0 = _mm_cvtps_epi32(vClamped0);
_mm_store_si128((__m128i*)(outBuffer+i), vFinal0); // Aligned store
}
}
}

void ConvertPcm32FixedToPcm32Float(float* outBuffer, const int32* inBuffer, uint sampleCount, bool bUseAvx)
{
const float fScale = (float)(1U<<31);

if (bUseAvx)
{
_mm256_zeroupper();
const __m256 vScale = _mm256_set1_ps(1/fScale);

for (uint i = 0; i < sampleCount; i+=8)
{
__m256i vIn0 = _mm256_load_si256(reinterpret_cast<const __m256i*>(inBuffer+i)); // Aligned load
__m256 vVal0 = _mm256_cvtepi32_ps(vIn0);
vVal0 = _mm256_mul_ps(vVal0, vScale);
_mm256_store_ps(outBuffer+i, vVal0); // Aligned store
}
_mm256_zeroupper();
}
else
{
const __m128 vScale = _mm_set1_ps(1/fScale);

for (uint i = 0; i < sampleCount; i+=4)
{
__m128i vIn0 = _mm_load_si128(reinterpret_cast<const __m128i*>(inBuffer+i)); // Aligned load
__m128 vVal0 = _mm_cvtepi32_ps(vIn0);
vVal0 = _mm_mul_ps(vVal0, vScale);
_mm_store_ps(outBuffer+i, vVal0); // Aligned store
}
}
}

Поэтому я запускаю запуск таймера, запускаю ConvertPcm32FloatToPcm32Fixed, а затем ConvertPcm32FixedToPcm32Float для прямого преобразования и завершения таймера. Версии функций SSE2 выполняются в общей сложности 15-16 микросекунд, но версии XVX занимают 22-23 микросекунды. Немного растерянно, я копнул немного дальше и обнаружил, как ускорить версии AVX, чтобы они работали быстрее, чем версии SSE2, но это обман. Я просто запускаю ConvertPcm32FloatToPcm32Fixed перед запуском таймера, затем запускаю таймер и снова запускаю ConvertPcm32FloatToPcm32Fixed, затем ConvertPcm32FixedToPcm32Float, останавливаю таймер. Как будто существует огромный штраф для SSE в AVX, если я сначала «заправлю» версию AVX пробным запуском, время выполнения AVX упадет до 12 микросекунд, в то время как выполнение аналогичных действий с эквивалентами SSE только уменьшит время микросекунда до 14, что делает AVX незначительным победителем здесь, но только если я обманываю. Я считал, что, возможно, AVX не так хорошо работает с кешем, как SSE, но использование _mm_prefetch тоже ничего не помогает.

Я что-то здесь упускаю?

4

Решение

Я не проверял ваш код, но так как ваш тест выглядит довольно коротким, возможно, вы видите Эффект разогрева с плавающей точкой что Агнер Фог обсуждает на стр.101 своего руководство по микроархитектуре (это относится к архитектуре Sandy Bridge). Я цитирую:

Процессор находится в холодном состоянии, когда он не видел плавающего
покажи инструкцию Задержка для 256-битного вектора
сложения и умножения первоначально на два часа дольше, чем
идеальное число, то на один час дольше, а через несколько сотен
инструкции с плавающей запятой процессор переходит в теплое состояние, где
задержки составляют 3 и 5 часов соответственно. Пропускная способность составляет половину
идеальное значение для 256-битных векторных операций в холодном состоянии. 128-бит
эффект разогрева меньше влияет на векторные операции.
задержка сложения и умножения 128-битных векторов составляет
большинство тактов дольше, чем идеальное значение, и пропускная способность
не уменьшается в холодном состоянии.

5

Другие решения

У меня сложилось впечатление, что, если компилятор не кодирует инструкции SSE с использованием формата инструкций VEX, как сказал Пол Р. — vmulps вместо mulps, попадание будет огромным.

При оптимизации небольших сегментов я склонен использовать этот замечательный инструмент Intel вместе с некоторыми хорошими старыми тестами.

https://software.intel.com/en-us/articles/intel-architecture-code-analyzer

Отчет, сгенерированный IACA, включает следующие обозначения:

«@ — инструкция SSE следовала инструкции AVX256, ожидается штраф в десятки циклов»

2