Побитовое преобразование типов с AVX2 и сохранение диапазона
Я хочу преобразовать вектор со знаком char в вектор без знака. Я хочу сохранить диапазон значений для каждого типа.
Я имею в виду диапазон значений знакового символа от -128 до +127, когда диапазон значений элемента без знака находится между 0 и 255.
Без встроенных функций я могу сделать это почти так:
#include <iostream>
int main(int argc,char* argv[])
{
typedef signed char schar;
typedef unsigned char uchar;
schar a[]={-1,-2,-3,4,5,6,-7,-8,9,10,-11,12,13,14,15,16,17,-128,19,20,21,22,23,24,25,26,27,28,29,30,31,32};
uchar b[32] = {0};
for(int i=0;i<32;i++)
b[i] = 0xFF & ~(0x7F ^ a[i]);
return 0;
}
Итак, используя AVX2, я написал следующую программу:
#include <immintrin.h>
#include <iostream>
int main(int argc,char* argv[])
{
schar a[]={-1,-2,-3,4,5,6,-7,-8,9,10,-11,12,13,14,15,16,17,-128,19,20,21,22,23,24,25,26,27,28,29,30,31,32};
uchar b[32] = {0};
__m256i _a = _mm256_stream_load_si256(reinterpret_cast<const __m256i*>(a));
__m256i _b;
__m256i _cst1 = _mm256_set1_epi8(0x7F);
__m256i _cst2 = _mm256_set1_epi8(0xFF);
_a = _mm256_xor_si256(_a,_cst1);
_a = _mm256_andnot_si256(_cst2,_a);
// The way I do the convertion is inspired by an algorithm from OpenCV.
// Convertion from epi8 -> epi16
_b = _mm256_srai_epi16(_mm256_unpacklo_epi8(_mm256_setzero_si256(),_a),8);
_a = _mm256_srai_epi16(_mm256_unpackhi_epi8(_mm256_setzero_si256(),_a),8);
// convert from epi16 -> epu8.
_b = _mm256_packus_epi16(_b,_a);
_mm256_stream_si256(reinterpret_cast<__m256i*>(b),_b);
return 0;
}
Когда я показываю переменную b, она была полностью пустой. Я проверяю также следующие ситуации:
#include <immintrin.h>
#include <iostream>
int main(int argc,char* argv[])
{
schar a[]={-1,-2,-3,4,5,6,-7,-8,9,10,-11,12,13,14,15,16,17,-128,19,20,21,22,23,24,25,26,27,28,29,30,31,32};
uchar b[32] = {0};
__m256i _a = _mm256_stream_load_si256(reinterpret_cast<const __m256i*>(a));
__m256i _b;
__m256i _cst1 = _mm256_set1_epi8(0x7F);
__m256i _cst2 = _mm256_set1_epi8(0xFF);
// The way I do the convertion is inspired by an algorithm from OpenCV.
// Convertion from epi8 -> epi16
_b = _mm256_srai_epi16(_mm256_unpacklo_epi8(_mm256_setzero_si256(),_a),8);
_a = _mm256_srai_epi16(_mm256_unpackhi_epi8(_mm256_setzero_si256(),_a),8);
// convert from epi16 -> epu8.
_b = _mm256_packus_epi16(_b,_a);
_b = _mm256_xor_si256(_b,_cst1);
_b = _mm256_andnot_si256(_cst2,_b);
_mm256_stream_si256(reinterpret_cast<__m256i*>(b),_b);
return 0;
}
а также:
#include <immintrin.h>
#include <iostream>
int main(int argc,char* argv[])
{
schar a[]={-1,-2,-3,4,5,6,-7,-8,9,10,-11,12,13,14,15,16,17,-128,19,20,21,22,23,24,25,26,27,28,29,30,31,32};
uchar b[32] = {0};
__m256i _a = _mm256_stream_load_si256(reinterpret_cast<const __m256i*>(a));
__m256i _b;
__m256i _cst1 = _mm256_set1_epi8(0x7F);
__m256i _cst2 = _mm256_set1_epi8(0xFF);
// The way I do the convertion is inspired by an algorithm from OpenCV.
// Convertion from epi8 -> epi16
_b = _mm256_srai_epi16(_mm256_unpacklo_epi8(_mm256_setzero_si256(),_a),8);
_a = _mm256_srai_epi16(_mm256_unpackhi_epi8(_mm256_setzero_si256(),_a),8);
_a = _mm256_xor_si256(_a,_cst1);
_a = _mm256_andnot_si256(_cst2,_a);
_b = _mm256_xor_si256(_b,_cst1);
_b = _mm256_andnot_si256(_cst2,_b);
_b = _mm256_packus_epi16(_b,_a);
_mm256_stream_si256(reinterpret_cast<__m256i*>(b[0]),_b);
return 0;
}
Мое расследование показало, что часть проблемы связана с операцией and_not. Но я не понимаю почему.
Переменная b должна содержать следующую последовательность: [127, 126, 125, 132, 133, 134, 121, 120, 137, 138, 117, 140, 141, 142, 143, 144, 145, 0, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160].
Заранее благодарю за любую помощь.
2 ответа
Да, "andnot" определенно выглядит отрывочно. поскольку _cst2
значения установлены в 0xFF
эта операция будет И ваша _b
вектор с нуля. Я думаю, что вы перепутали порядок аргументов. Это первый аргумент, который переворачивается. Смотрите ссылку.
Я не понимаю остальную часть болтовни с преобразованиями и т.д. Вам просто нужно это:
__m256i _a, _b;
_a = _mm256_stream_load_si256( reinterpret_cast<__m256i*>(a) );
_b = _mm256_xor_si256( _a, _mm256_set1_epi8( 0x7f ) );
_b = _mm256_andnot_si256( _b, _mm256_set1_epi8( 0xff ) );
_mm256_stream_si256( reinterpret_cast<__m256i*>(b), _b );
Альтернативное решение - просто добавить 128, но я не уверен в последствиях переполнения в этом случае:
__m256i _a, _b;
_a = _mm256_stream_load_si256( reinterpret_cast<__m256i*>(a) );
_b = _mm256_add_epi8( _a, _mm256_set1_epi8( 0x80 ) );
_mm256_stream_si256( reinterpret_cast<__m256i*>(b), _b );
И последнее, что важно, это то, что ваш a
а также b
массивы ДОЛЖНЫ иметь 32-байтовое выравнивание. Если вы используете C++11, вы можете использовать alignas
:
alignas(32) signed char a[32] = { -1,-2,-3,4,5,6,-7,-8,9,10,-11,12,13,14,15,16,17,
-128,19,20,21,22,23,24,25,26,27,28,29,30,31,32 };
alignas(32) unsigned char b[32] = {0};
В противном случае вам нужно будет использовать невыровненные инструкции по загрузке и хранению, т.е. _mm256_loadu_si256
а также _mm256_storeu_si256
, Но у них нет тех же временных свойств кэша, что и у потоковых инструкций.
Вы просто говорите о добавлении 128
на каждый байт, верно? Это смещает диапазон от [-128..127]
в [0..255]
, Хитрость для добавления 128, когда вы можете использовать только 8-битные операнды, состоит в том, чтобы вычесть -128.
Тем не менее, добавив 0x80
работает так же, когда результат усекается до 8 бит. (из-за дополнения до двух). Добавление это хорошо, потому что не имеет значения, в каком порядке находятся операнды, поэтому компилятор может использовать инструкцию загрузки и добавления (складывание операнда памяти в загрузку).
Сложение / вычитание -128 с остановкой переноса / заимствования границей элемента эквивалентно xor
(ака безносный добавить). С помощью pxor
может быть небольшим преимуществом для Intel Core2 через Broadwell, так как Intel, должно быть, думала, что это того стоило добавить paddb/w/d/q
аппаратное обеспечение на порту 0 для Skylake (давая им один на 0.333c пропускную способность, как pxor
). (Спасибо @harold за то, что указал на это). Обе инструкции требуют только SSE2.
XOR также потенциально полезен для очистки выравнивания SWAR или для SIMD-архитектур, в которых нет операции добавления / вычитания в байтовом размере.
Вы не должны использовать _a
для вашего имени переменной. _
имена защищены. Я склонен использовать такие имена, как veca
или же va
и желательно что-то более описательное для временных. (Подобно a_unpacked
).
__m256i signed_bytes = _mm256_loadu_si256(reinterpret_cast<const __m256i*>(a));
__m256i unsigned_bytes = _mm256_add_epi8(signed_bytes, _mm256_set1_epi8(-128));
Да, это так просто, вам не нужны битхаки с двумя комплементами. Во-первых, вашему пути нужны две отдельные маски 32B, что увеличивает объем кеша. (Но посмотрите, каковы лучшие последовательности команд для генерации векторных констант на лету? Вы (или компилятор) могли бы сгенерировать вектор -128
байт с использованием 3 инструкций или широковещательная загрузка из константы 4B.)
Использовать только _mm256_stream_load_si256
для ввода / вывода (например, чтение из видео RAM). Не используйте его для чтения из "нормальной" (обратной записи) памяти; это не делает то, что вы думаете, что делает. (Я не думаю, что это имеет какой-то конкретный недостаток, хотя. Он просто работает как нормальный vmovdqa
нагрузка). Я поместил некоторые ссылки на это в другом ответе, который я недавно написал.
Хранилища потоков полезны для нормальных (с обратной записью) областей памяти. Тем не менее, это хорошая идея, только если вы не собираетесь читать эту память снова в ближайшее время. Если это так, вы, вероятно, должны выполнить это преобразование из подписанного в неподписанное на лету в коде, который читает эти данные, потому что это супер-дешево. Просто сохраните ваши данные в одном или другом формате и конвертируйте на лету в код, который нуждается в этом, другим способом. Необходимость только одной копии в кеше - огромный выигрыш по сравнению с сохранением одной инструкции в нескольких циклах.
Также поищите в Google "блокировку кеша" (или циклическое разбиение по циклам) и прочитайте об оптимизации вашего кода для работы небольшими порциями для увеличения плотности вычислений. (Делайте как можно больше с данными, пока они в кеше.)