我想对宽度为128、256或512位的CPU寄存器(xmm、ymm或zmm)执行单个位、位对和半字节(4位)的任意排列;这应该尽可能快。为此,我正在研究SIMD指令。有没有人知道一种方法来做到这一点/一个实现它的库?我在Windows上使用MSVC,在Linux上使用GCC,主机语言是C或C++。谢谢!
我给出了一个任意的排列,需要对大量的位向量/位向量对/半字节进行 Shuffle 。我知道如何对64位值中的位执行此操作,例如。使用Benes网络。
或者在更宽的SIMD寄存器上混洗8位或更大的块,例如使用Agner Fog的GPLed VectorClass库(https://www.agner.org/optimize/vectorclass.pdf)作为模板元编程函数,该函数在给定shuffle作为模板参数的情况下,从AVX 2通道内字节shuffle和/或较大元素通道交叉shuffle中构建shuffle。
然而,对排列进行更细粒度的细分(细分为1、2或4位块)似乎很难在宽向量上实现。
我能够对排列进行预处理,例如。为了提取位掩码,根据需要计算索引,例如对于Benes网络,或其他任何东西-也很乐意用另一种高级语言来做,所以假设以最方便解决问题的任何格式给出置换;包括小的查找表。
我希望代码的运行速度要比类似
// actually 1 bit per element, not byte. I want a 256-bit bit-shuffle
const uint8_t in[256] = get_some_vector(); // not a compile-time constant
const uint8_t perm[256] = ...; // compile-time constant
uint8_t out[256];
for (size_t i = 0; i < 256; i ++)
out[i] = in[perm[i]];
如前所述,我有一个<= 64位的解决方案(即64位,32位对和16个半字节)。对于大小为8、16、32等的块也解决了该问题。在更宽的SIMD寄存器上。
编辑:澄清一下,置换是一个编译时常数(但不只是一个特定的,我将对每个给定的置换编译一次程序)。
2条答案
按热度按时间vcudknz31#
AVX 2 256位置换情况
我认为不可能编写一个有效的通用SSE 4/AVX 2/AVX-512算法,适用于所有向量大小(128,256,512位)和元素粒度(位,位对,半字节,字节)。一个问题是,对于例如字节大小元素存在的许多AVX 2指令对于双字元素不存在,反之亦然。
下面讨论AVX 2 256位置换情况。也许可以把这个案例的想法用于其他案例。
其思想是从输入向量
x
中每步提取32(置换)位。在每个步骤中,从置换向量pos
读取32个字节。这些pos
字节的位7..3确定需要x
中的哪个字节。右字节由仿真的256位宽AVX 2通道交叉字节混洗coded here by Ermlg选择。pos
字节的位2..0确定要查找的位。使用_mm256_movemask_epi8
,32位被收集在一个_uint32_t
中。此步骤重复8次,以获得所有256个置换位。代码看起来不太优雅。尽管如此,如果有一个明显更快的AVX 2方法,比如说快两倍,我会感到惊讶。
具有示例置换的输出看起来是正确的:
效率
如果你仔细观察这个算法,你会发现一些运算只依赖于置换向量
pos
,而不依赖于x
。这意味着应用具有变量x
和固定pos
的置换应该比应用具有变量x
和pos
的置换更有效。下面的代码说明了这一点:
使用clang和gcc,这实际上编译为nice code:第237行的循环
.L5
只包含16个vpshufb
s,而不是24个。此外,vpaddb
s被提升出循环。请注意,循环中也只有一个vpermq
。我不知道MSVC是否会在循环外提升这么多指令。如果没有,可以通过手动修改代码来提高循环的性能。应该这样做,使得只依赖于
pos
而不依赖于x
的操作被提升到循环之外。关于英特尔Skylake的性能:该循环的吞吐量可能受到每个循环迭代约32个端口5个微操作的限制。这意味着在循环上下文中,例如
perm_array
的吞吐量大约是每32个CPU周期256个置换位,或者每CPU周期8个置换位。使用AVX 2指令的128位排列
该代码与256位置换的情况非常相似。虽然只有128位被置换,但AVX 2寄存器的全部256位宽用于实现最佳性能。这里不模拟字节混洗。这是因为在128位通道内存在有效的单个指令来进行字节混洗:
vpshufb
.函数
perm_array_128
测试固定置换和可变输入x
的比特置换的性能。如果我们假设一个Intel Skylake CPU,则汇编循环包含大约11个端口5(p5)微操作。这些11 p5微操作至少需要11个CPU周期(吞吐量)。因此,在最好的情况下,我们得到了每周期大约12个置换位的吞吐量,这是256位置换情况的大约1.5倍快。一些任意排列的示例输出:
wvyml7n52#
AVX2
上面的答案很好,但如果数据在内存中,我们可以做得更好一点:
与
vpermq
不同,从内存向ymm寄存器的所有256位广播128位不使用混洗µop。不幸的是,编译器有时似乎会在存储vpmovmskb
结果之前将其插入到向量寄存器中,这可以在接受的答案的编译器输出中看到。可以在存储之间插入asm内存占用器(asm volatile ("" ::: "memory");
),不幸的是,这会破坏指令重新排序,或者(在我的应用程序中)链接汇编例程。在使用
-march=core-avx2
的Cascade Lake上,我得到**~12.3位/周期**。AVX512BW / VBMI
我们可以用AVX 512做得更好。大多数示例将使用以下函数,类似于原始AVX 2答案中
get_32_128_bits
的前几行。对于循环不变排列,这些值应该被提升而不是重复计算。
64位
在64位及更小的特殊情况下,我们可以使用
vpshufbitqmb
选择64位到掩码寄存器中,并使用kmovq m64, k
直接存储到内存中。如果vpshufbitqmb
不可用,我们可以用字节混洗来模拟它,然后用所有需要的位的掩码来模拟vptestmb
。(vptestmb
计算两个向量寄存器的逻辑与,并对每个非零字节向掩码寄存器写入1。)作为循环的一部分的主力:
在Ice Lake上观察到的性能为~1.38周期/元素,或**~46位/周期**(由于无法访问性能计数器而出现一些错误)。回退实现以2周期/元素或32位/周期运行,因为
vptestmb
和vpshufb
竞争同一端口。128位
循环始终以4个周期/元素或32位/周期运行,瓶颈与之前相同(
vpshufb
/vptestmb
)。256位
128位通道交叉字节粒度 Shuffle (
vpermb
和朋友)仅在AVX 512 VBMI(Ice Lake和更高版本; Zen 4). AVX 512 BW版本与AVX 2版本没有太大区别:256位输入在两个128位组块中广播,使用合并掩码vpshufb
混洗。我得到了12个周期/ 256位元素(21位/周期),这甚至没有AVX 2纯版本的两倍,因为比较到掩码寄存器与混洗竞争。* 不 * 使用端口5的向量到掩码操作似乎是
vpmovb2m
等。似乎没有一个有利可图的方法来获得HSB的相关位使用单个指令(没有可变字节移位),但我们可以在两个以上启用DO_INTEL_OPT
-这将效率提高到理论上10个周期/元素(25.6位/周期)。这可能是或可能不值得的复杂性,我敢肯定是严格更糟的AMD。VBMI解决方案不起眼,以32位/周期运行:
512位
我不需要这么广泛的排列,但用VBMI也不难。唯一的问题是你必须将排列索引存储为每个16位,但是一旦你将其转换为字节+位索引的形式,它应该和以前的解决方案一样--只需用普通的旧的512位
vmovdqu
替换vbroadcasti64x4
加载即可。