1

我想将有符号字符向量转换为无符号字符向量。我想保留每种类型的值范围。

我的意思是当 unsigned char 元素的值范围在 0 - 255 之间时,signed char 的值范围是 -128 和 +127。

如果没有内在函数,我几乎可以这样做:

#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]。

提前感谢您的帮助。

4

2 回答 2

1

您只是在谈论添加128到每个字节,对吗?这会将范围从 转移[-128..127][0..255]。当您只能使用 8 位操作数时,添加 128 的技巧是减去 -128。

但是,0x80当结果被截断为 8 位时,添加也有效。(因为补码)。添加很好,因为操作数的顺序无关紧要,因此编译器可以使用加载和添加指令(将内存操作数折叠到加载中)。

加/减 -128,进位/借位由元素边界停止,相当于xor(又名无进位加法)。通过 Broadwell 在 Intel Core2 上使用pxor可能是一个小优势,因为 Intel 一定认为paddb/w/d/q在端口 0 上为 Skylake 添加硬件是值得的(每 0.333c 吞吐量给它们一个,例如pxor)。(感谢@harold 指出这一点)。两条指令都只需要 SSE2。

XOR 对于SWAR未对齐的清理或没有字节大小的加/减操作的 SIMD 架构也可能有用。


你不应该使用_a你的变量名。 _名称是保留的。我倾向于使用vecaor之类的名称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));

是的,就是这么简单,你不需要补码 bithacks。一方面,您的方式需要两个单独的 32B 掩码,这会增加您的缓存占用空间。(但请参阅动态生成向量常量的最佳指令序列是什么? 您(或编译器)可以-128使用 3 条指令或从 4B 常量的广播加载来生成字节向量。)


_mm256_stream_load_si256用于 I/O(例如从视频 RAM 中读取)。不要将它用于从“正常”(回写)内存中读取;它没有做你认为它做的事情。(不过,我认为它没有任何特别的缺点。它就像正常vmovdqa负载一样工作)。我在我最近写的另一个答案中放了一些关于这个的链接。

流式存储对于普通(写回)内存区域很有用。但是,仅当您不打算在短期内再次读取该内存时,它们才是一个好主意。如果是这种情况,您可能应该在读取此数据的代码中即时执行从有符号到无符号的转换,因为它非常便宜。只需将数据保存为一种或另一种格式,然后以另一种方式即时转换需要它的代码。与在某些循环中保存一条指令相比,只需要它的一个副本在缓存中是一个巨大的胜利。

另外谷歌“缓存阻塞”(又名循环平铺)并阅读有关优化代码以小块工作以增加计算密度的信息。(尽可能多地处理缓存中的数据。)

于 2016-02-04T08:44:54.537 回答
0

是的,“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 );

最后一件重要的事情是您的ab数组必须具有 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. 但是那些不具有与流指令相同的非临时缓存属性。

于 2016-02-04T03:16:46.740 回答