Вы можете заменить стандартный способ преобразования epi16-> epi32-> float и умножения на 1.f/2048.f
, вручную составив float.
Это работает, потому что делитель имеет степень 2, поэтомуручное создание числа с плавающей запятой означает просто другой показатель степени.
Благодаря @PeterCordes, вот оптимизированная версия этой идеи для AVX2, использующая XOR для установки старших байтов 32-разрядного числа с плавающей запятой в то же время, что и отражениезнаковый бит целочисленного значения.FP SUB превращает эти младшие биты мантиссы в правильное значение FP:
// get input:
__m128i val = _mm_loadu_si128((__m128i*)input);
// interleave with 0x0000
__m256i val_unpacked = _mm256_cvtepu16_epi32(val);
// 0x4580'8000
const __m256 magic = _mm256_set1_ps(float((1<<23) + (1<<15))/2048.f);
const __m256i magic_i = _mm256_castps_si256(magic);
/// convert by xor-ing and subtracting magic value:
// VPXOR avoids port5 bottlenecks on Intel CPUs before SKL
__m256 val_f = _mm256_castsi256_ps(_mm256_xor_si256(val_unpacked, magic_i));
__m256 converted = _mm256_sub_ps(val_f, magic);
// store:
_mm256_storeu_ps(output, converted);
Посмотрите это в проводнике компилятора Godbolt с помощью gcc и clang ;на Skylake i7-6700k цикл элементов 2048, который горячий в кеше, занимает ~ 360 тактов, с той же скоростью (с точностью до погрешности измерения), что и версия @ wim, которая выполняет стандартное расширение / преобразование / умножение (с аналогичным количествомцикл раскатывания).Протестировано @PeterCordes с Linux perf
.Но на Райзене это может быть значительно быстрее, потому что мы избегаем _mm256_cvtepi32_ps
(Райзен имеет пропускную способность 1 на 2 такта для vcvtdq2ps ymm
: http://agner.org/optimize/.)
Xor 0x8000
с нижней половиной эквивалентнок добавлению / вычитанию 0x8000
, поскольку переполнение / перенос игнорируется. И по совпадению, это позволяет использовать одну и ту же магическую константу для XOR-ввода и вычитания.
Как ни странно, gcc и clang предпочитают заменять вычитание надобавление -magic
, которое не будет повторно использовать константу ... Они предпочитают использовать add
, потому что оно коммутативное, но в этом случае нет никакой выгоды, потому что они не используют его с операндом памяти.
Вот версия SSE2, которая выполняет переворачивание со знаком / без знака отдельно от установки старших 2 байтов 32-разрядного битового шаблона FP.
Мы используем один _mm_add_epi16
, два_mm_unpackXX_epi16
и два _mm_sub_ps
для 8 значений (_mm_castsi128_ps
не используются, и _mm_set
будет кэшироваться в регистрах):
// get input:
__m128i val = _mm_loadu_si128((__m128i*)input);
// add 0x8000 to wrap to unsigned short domain:
// val = _mm_add_epi16(val, _mm_set1_epi16(0x8000));
val = _mm_xor_si128(val, _mm_set1_epi16(0x8000)); // PXOR runs on more ports, avoids competing with FP add/sub or unpack on Sandybridge/Haswell.
// interleave with upper part of float(1<<23)/2048.f:
__m128i lo = _mm_unpacklo_epi16(val, _mm_set1_epi16(0x4580));
__m128i hi = _mm_unpackhi_epi16(val, _mm_set1_epi16(0x4580));
// interpret as float and subtract float((1<<23) + (0x8000))/2048.f
__m128 lo_f = _mm_sub_ps(_mm_castsi128_ps(lo), _mm_set_ps1(float((1<<23) + (1<<15))/2048.f));
__m128 hi_f = _mm_sub_ps(_mm_castsi128_ps(hi), _mm_set_ps1(float((1<<23) + (1<<15))/2048.f));
// store:
_mm_storeu_ps(output, lo_f);
_mm_storeu_ps(output+4, hi_f);
Демонстрация использования: https://ideone.com/b8BfJd
Если ваш вклад будетесли бы не было подписано short , то _mm_add_epi16
не понадобилось бы (и, конечно, 1<<15
в _mm_sub_ps
необходимо было бы удалить).Тогда у вас будет ответ Марата на SSE: преобразовать короткое целое число в число с плавающей точкой .
Это может легко быть перенесено в AVX2 с вдвое большим числом преобразований на итерацию, нонеобходимо позаботиться о порядке элементов вывода (спасибо @wim за указание на это).
Кроме того, для чистого решения SSE можно просто использовать _mm_cvtpi16_ps
, но это Intelбиблиотечная функция.Нет единой инструкции, которая делает это.
// cast input pointer:
__m64* input64 = (__m64*)input;
// convert and scale:
__m128 lo_f = _mm_mul_ps(_mm_cvtpi16_ps(input64[0]), _mm_set_ps1(1.f/2048.f));
__m128 hi_f = _mm_mul_ps(_mm_cvtpi16_ps(input64[1]), _mm_set_ps1(1.f/2048.f));
Я не тестировал ни одно решение (ни проверял теоретические пропускные способности или задержки)