使用 AVX2 和范围保存的按位类型转换
bitwise type convertion with AVX2 and range preservation
我想将 signed char 向量转换为 unsigned char 向量。
我想保留每种类型的取值范围。
我的意思是当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]。
在此先感谢您的帮助。
是的,"andnot" 看起来很粗略。由于 _cst2
值设置为 0xFF
,此操作会将您的 _b
向量与零相乘。我认为你混淆了论点的顺序。这是 first 论点被倒置了。 See the reference.
我也不明白其他关于转换的废话 等等。你只需要这个:
__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]
。当你只能使用8位操作数时加128的技巧是减去-128.
但是,当结果被截断为 8 位时,添加 0x80
也同样有效。 (因为补码)。添加很好,因为操作数的顺序无关紧要,因此编译器可以使用加载和添加指令(将内存操作数折叠到加载中)。
Adding/subtracting -128,carry/borrow 由元素边界停止,等同于 xor
(也称为无进位加法)。通过 Broadwell 使用 pxor
可能是英特尔酷睿 2 的一个小优势,因为英特尔一定认为在 Skylake 的端口 0 上添加 paddb/w/d/q
硬件是值得的(每 0.333c 吞吐量给它们一个,比如 pxor
). (感谢@harold 指出这一点)。两条指令都只需要 SSE2。
XOR 也可能对 SWAR 未对齐清理或没有字节大小 add/subtract 操作的 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));
是的,就是这么简单,您不需要二进制补码 bithack。一方面,您的方式需要两个单独的 32B 掩码,这会增加您的缓存占用空间。 (但是请参阅 您(或编译器)可以使用 3 条指令生成 -128
字节的向量,或者从 4B 常量生成广播负载。)
仅对 I/O 使用 _mm256_stream_load_si256
(例如从视频 RAM 读取)。不要用它来读取 "normal"(回写)内存;它不做你认为它做的事。 (不过,我认为它没有任何特别的缺点。它就像正常的 vmovdqa
负载一样工作)。我在 another answer I wrote recently.
中放置了一些相关链接
流式存储 对普通(回写)内存区域很有用。但是,如果您近期不打算再次阅读该内存,那么 仅 它们是个好主意。如果是这种情况,您可能应该在读取此数据的代码中即时执行从有符号到无符号的转换,因为它非常便宜。只需将您的数据保留为一种格式或另一种格式,然后在需要它的代码中即时转换为另一种格式。与在某些循环中保存一条指令相比,在缓存中只需要它的一个副本是一个巨大的胜利。
还有 google "cache blocking"(又名循环平铺)并阅读有关优化代码以在小块中工作以增加计算密度的信息。 (尽可能多地处理缓存中的数据。)
我想将 signed char 向量转换为 unsigned char 向量。 我想保留每种类型的取值范围。
我的意思是当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]。
在此先感谢您的帮助。
是的,"andnot" 看起来很粗略。由于 _cst2
值设置为 0xFF
,此操作会将您的 _b
向量与零相乘。我认为你混淆了论点的顺序。这是 first 论点被倒置了。 See the reference.
我也不明白其他关于转换的废话 等等。你只需要这个:
__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]
。当你只能使用8位操作数时加128的技巧是减去-128.
但是,当结果被截断为 8 位时,添加 0x80
也同样有效。 (因为补码)。添加很好,因为操作数的顺序无关紧要,因此编译器可以使用加载和添加指令(将内存操作数折叠到加载中)。
Adding/subtracting -128,carry/borrow 由元素边界停止,等同于 xor
(也称为无进位加法)。通过 Broadwell 使用 pxor
可能是英特尔酷睿 2 的一个小优势,因为英特尔一定认为在 Skylake 的端口 0 上添加 paddb/w/d/q
硬件是值得的(每 0.333c 吞吐量给它们一个,比如 pxor
). (感谢@harold 指出这一点)。两条指令都只需要 SSE2。
XOR 也可能对 SWAR 未对齐清理或没有字节大小 add/subtract 操作的 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));
是的,就是这么简单,您不需要二进制补码 bithack。一方面,您的方式需要两个单独的 32B 掩码,这会增加您的缓存占用空间。 (但是请参阅 -128
字节的向量,或者从 4B 常量生成广播负载。)
仅对 I/O 使用 _mm256_stream_load_si256
(例如从视频 RAM 读取)。不要用它来读取 "normal"(回写)内存;它不做你认为它做的事。 (不过,我认为它没有任何特别的缺点。它就像正常的 vmovdqa
负载一样工作)。我在 another answer I wrote recently.
流式存储 对普通(回写)内存区域很有用。但是,如果您近期不打算再次阅读该内存,那么 仅 它们是个好主意。如果是这种情况,您可能应该在读取此数据的代码中即时执行从有符号到无符号的转换,因为它非常便宜。只需将您的数据保留为一种格式或另一种格式,然后在需要它的代码中即时转换为另一种格式。与在某些循环中保存一条指令相比,在缓存中只需要它的一个副本是一个巨大的胜利。
还有 google "cache blocking"(又名循环平铺)并阅读有关优化代码以在小块中工作以增加计算密度的信息。 (尽可能多地处理缓存中的数据。)