Преобразование побитового типа с AVX2 и сохранение диапазона

Я хочу преобразовать вектор подписанного char в вектор беззнакового char. Я хочу сохранить диапазон значений для каждого типа.

Я имею в виду, что диапазон значений подписанного char равен -128 и +127, когда диапазон значений без знака char находится между 0 - 255.

Без intrinsics я могу сделать это почти так:

#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;
}

Когда я показываю varaible 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].

Заранее благодарим за помощь.

Ответ 1

Да, "andnot" определенно выглядит отрывочно. Поскольку значения _cst2 установлены на 0xFF, эта операция будет И ваш вектор _b с нулем. Я думаю, вы перепутали порядок аргументов. Это первый аргумент, который инвертируется. См. ссылку.

Я тоже не понимаю остальных guff с преобразованиями. Вам просто нужно это:

__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-байтовое выравнивание. Если вы используете С++ 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. Но они не имеют одинаковых невременных свойств кеша, как инструкции потока.

Ответ 2

Вы просто говорите о добавлении 128 в каждый байт, правильно? Это сдвигает диапазон от [-128..127] до [0..255]. Трюк для добавления 128, когда вы можете использовать только 8-битные операнды, состоит в вычитании -128.

Однако добавление 0x80 также работает, когда результат усечен до 8 бит. (из-за двух дополнений). Добавление полезно, потому что не имеет значения, в каком порядке находятся операнды, поэтому компилятор может использовать команду load-and-add (сворачивание операнда памяти в нагрузку).

Добавление/вычитание -128 с переносом/заимствованием, остановленным границей элемента, эквивалентно xor (aka unless add). Использование pxor может быть небольшим преимуществом для Intel Core2 через Broadwell, так как Intel, должно быть, подумала, что стоит добавить аппаратное обеспечение paddb/w/d/q на port0 для Skylake (давая им один на 0,3333c пропускную способность, например pxor). (Спасибо @harold за указание на это). Обе инструкции требуют только SSE2.

XOR также потенциально полезен для SWAR unaligned cleanup, или для архитектур 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 для ввода-вывода (например, чтение из видеопамяти). Не используйте его для чтения из "нормальной" (обратной) памяти; он не делает то, что, по вашему мнению, делает. (Я не думаю, что это имеет какой-то определенный недостаток, но это работает как обычная загрузка vmovdqa). Я добавил несколько ссылок об этом в еще один ответ, который я написал недавно.

Потоковые хранилища полезны для нормальных (обратных) областей памяти. Тем не менее, это хорошая идея, только если вы не собираетесь читать эту память снова в ближайшее время. Если это так, вы должны, вероятно, сделать это преобразование из подписанного в unsigned на лету в коде, который читает эти данные, потому что он супер-дешевый. Просто держите свои данные в одном формате или другом, и конвертируйте "на лету" в код, который нуждается в этом другим способом. Только одна копия его в кеше - огромная победа по сравнению с сохранением одной инструкции в некоторых циклах.

Также блокировка кеша google (aka loop tiling) и чтение об оптимизации вашего кода для работы в небольших кусках для увеличения вычислительной плотности. (Делайте как можно больше материалов с данными, находясь в кеше.)