Побитовое преобразование типов с 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 "блокировку кеша" (или циклическое разбиение по циклам) и прочитайте об оптимизации вашего кода для работы небольшими порциями для увеличения плотности вычислений. (Делайте как можно больше с данными, пока они в кеше.)

Другие вопросы по тегам