使用 AVX2 和范围保留的按位类型转换
Posted
技术标签:
【中文标题】使用 AVX2 和范围保留的按位类型转换【英文标题】:bitwise type convertion with AVX2 and range preservation 【发布时间】:2016-02-04 02:25:40 【问题描述】:我想将有符号字符向量转换为无符号字符向量。 我想保留每种类型的值范围。
我的意思是,当 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]。
提前感谢您的帮助。
【问题讨论】:
您能否更详细地解释“我想保留值范围”的含义。例如有符号字符值-2
会转换成什么?
@M.M:我认为他的意思是与abs()
相对,或者将负数饱和到 0 或其他东西。从最后一段“b应该包含”,我们可以看出他只是想加128。
我编译了一个与您的原始代码类似但不使用内在函数的程序,clang/llvm 优化器足够聪明,可以重写代码以使用 avx 指令来执行打包操作。你确定你真的可以比你的编译器做得更好吗?
【参考方案1】:
您只是在谈论将128
添加到每个字节,对吗?这会将范围从[-128..127]
转移到[0..255]
。当只能使用 8 位操作数时,加 128 的技巧是减去 -128。
但是,当结果被截断为 8 位时,添加 0x80
也可以。 (因为补码)。添加是好的,因为操作数的顺序无关紧要,因此编译器可以使用加载和添加指令(将内存操作数折叠到加载中)。
加/减-128,进位/借位由元素边界停止,相当于xor
(又名无进位加法)。通过 Broadwell 在 Intel Core2 上使用 pxor
可能是一个小优势,因为 Intel 一定认为值得在端口 0 上为 Skylake 添加 paddb/w/d/q
硬件(给它们一个每 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));
是的,就是这么简单,您不需要补码 bithacks。一方面,您的方式需要两个单独的 32B 掩码,这会增加您的缓存占用空间。 (但请参阅What are the best instruction sequences to generate vector constants on the fly?,您(或编译器)可以使用 3 条指令生成 -128
字节的向量,或从 4B 常量进行广播加载。)
仅将 _mm256_stream_load_si256
用于 I/O(例如从视频 RAM 读取)。不要将它用于从“正常”(回写)内存中读取;它没有做你认为它做的事情。 (不过,我认为它没有任何特别的缺点。它就像普通的vmovdqa
负载一样工作)。我在another answer I wrote recently 中放了一些相关链接。
流式存储对于普通(写回)内存区域很有用。但是,仅如果您不打算在短期内再次读取该内存,它们是一个好主意。如果是这种情况,您可能应该在读取此数据的代码中即时执行从有符号到无符号的转换,因为它非常便宜。只需将数据保存为一种或另一种格式,然后以另一种方式即时转换需要它的代码。与在某些循环中保存一条指令相比,只需要在缓存中保存一份副本是一个巨大的胜利。
另外,谷歌“缓存阻塞”(又名循环平铺)并阅读有关优化代码以小块工作以增加计算密度的信息。 (尽可能多地处理缓存中的数据。)
【讨论】:
很高兴知道。谢谢你的信息。实际上这是代码只是一个实验。该代码将用于处理图像,这就是我使用指令 _mm256_stream_load_si256 的原因。目的是将有符号字符移动到无符号字符以处理直方图。但我会仔细查看您的帖子以及缓存阻塞。 我只是觉得做 128 的减法而不是加法不是更好吗?我的意思是:__m256i unsigned_bytes = _mm256_sub_epi8(signed_bytes, _mm256_set1_epi8(128));
而不是:__m256i unsigned_bytes = _mm256_add_epi8(signed_bytes, _mm256_set1_epi8(-128));
在 intel 内在函数指南网页上,它写道,substration 没有吞吐量,而加法的吞吐量为 0.5。
@Jonny_S:那么您是否真的使用视频驱动程序 API 将 USWC 视频 RAM 映射到您的进程中?如果不是,则不要使用 stream_load。此外,在直方图代码中动态转换有符号和无符号范围。 re: add vs. sub:vpaddb
和 vpsubb
在所有 CPU 上具有相同的吞吐量、延迟和执行单元要求,因为这是唯一合理的硬件设计。 IDK 为什么内在函数指南用-
列出它,但这并不意味着它具有无限的吞吐量!查看agner.org/optimize 以获得更好的说明表(以及如何理解其含义)。
您也可以与 -128 进行异或运算,这在 Haswell 上具有更高的吞吐量(在这种情况下可能并不重要,但很好)
@PeterCordes 不,他们不能,或者至少,我在 SnB、IvB 和 Haswell 上将它们列为 p15,它们只在 Skylake 中成为 p015【参考方案2】:
是的,“andnot”确实看起来很粗略。由于_cst2
值设置为0xFF
,因此此操作将与您的_b
向量与零。我认为你混淆了论点的顺序。倒置的是 first 参数。 See the reference.
我也不明白转换的其余部分 etc。你只需要这个:
__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
。但是那些不具有与流指令相同的非临时缓存属性。
【讨论】:
您好,非常感谢您的回答。我做了一个无符号短的转换,因为我不确定我是否可以在不改变类型的情况下进行所有操作。你对对齐也是正确的。非常感谢您的帮助:) NT 从正常(写回)内存加载没有帮助,所以 OP 不太可能真的需要它们,但你说得对,流存储必须对齐。另外,添加0x80
是正确的,我检查了。它与减去0x80
(-128
) 的作用完全相同。因此,动态转换甚至更便宜:当编译器将加载折叠到 add 作为内存操作数时,可以使用 vpaddb dest, ymm7, m256
指令来完成。 (按其他顺序减去是行不通的。)以上是关于使用 AVX2 和范围保留的按位类型转换的主要内容,如果未能解决你的问题,请参考以下文章
为啥 AVX2 和 SSE2 按位 OR 运算符并不比简单的快?操作员?