如果你有一个输入数组和一个输出数组,但是你只想写那些通过某种条件的元素,那么在AVX2中这样做最有效的方法是什么?
我在SSE中看到过这样做: (来源:https://deplinenoise.files.wordpress.com/2015/03/gdc2015_afredriksson_simd.pdf)
__m128i LeftPack_SSSE3(__m128 mask, __m128 val)
{
// Move 4 sign bits of mask to 4-bit integer value.
int mask = _mm_movemask_ps(mask);
// Select shuffle control data
__m128i shuf_ctrl = _mm_load_si128(&shufmasks[mask]);
// Permute to move valid values to front of SIMD register
__m128i packed = _mm_shuffle_epi8(_mm_castps_si128(val), shuf_ctrl);
return packed;
}
这对于4宽的SSE来说似乎很好,因此只需要16个入口LUT,但对于8宽的AVX,LUT变得非常大(256个条目,每个32个字节或8k)。
我很惊讶AVX似乎没有简化此过程的说明,例如带有包装的蒙面商店。
我认为通过一些改变来计算左边设置的符号位数,你可以生成必要的置换表,然后调用_mm256_permutevar8x32_ps。但这也是我认为的一些指示......
有没有人知道用AVX2做这个的任何技巧?或者最有效的方法是什么?
以下是上述文件中左包装问题的说明:
由于
答案 0 :(得分:25)
AVX2 + BMI2。请参阅我对AVX512的其他答案。 (更新:在64位版本中保存pdep
。)
我们可以使用AVX2 vpermps
(_mm256_permutevar8x32_ps
)(或等价的整数,vpermd
)来进行交叉变量随机播放。
我们可以动态生成面具,因为BMI2 pext
(Parallel Bits Extract)为我们提供了所需操作的按位版本。
请注意,AMD CPU上pdep
/ pext
非常慢,例如6 uops / 18周期延迟和Ryzen上的吞吐量。这个实现将在AMD上表现可怕。对于AMD,使用pshufb
或vpermilps
LUT的128位向量,或者如果您的掩码输入是向量掩码(不是注释)中的一些AVX2变量建议,则最好使用注释中讨论的一些AVX2变量建议从内存中已经计算过的位掩码)。在Zen2之前的AMD无论如何都只有128位向量执行单元,并且256位跨越通道的shuffle很慢。因此,对于当前的AMD,128位向量对此非常有吸引力。
对于具有32位或更宽元素的整数向量:1)_mm256_movemask_ps(_mm256_castsi256_ps(compare_mask))
。
或者2)使用_mm256_movemask_epi8
然后将第一个PDEP常量从0x0101010101010101更改为0x0F0F0F0F0F0F0F0F以分散4个连续位的块。将乘法值乘以0xFFU更改为expanded_mask |= expanded_mask<<4;
或expanded_mask *= 0x11;
(未测试)。无论哪种方式,使用带有VPERMD的shuffle掩码而不是VPERMPS。
对于64位整数或double
元素,一切仍然正常工作;比较掩码恰好总是具有相同的32位元素对,因此生成的shuffle将每个64位元素的两半放在正确的位置。 (因此,您仍然使用VPERMPS或VPERMD,因为VPERMPD和VPERMQ仅适用于即时控制操作数。)
对于16位元素,您可以使用128位向量进行调整。
从打包的3位索引的常量开始,每个位置都有自己的索引。即[ 7 6 5 4 3 2 1 0 ]
,其中每个元素是3位宽。 0b111'110'101'...'010'001'000
。
使用pext
将我们想要的索引提取到整数寄存器底部的连续序列中。例如如果我们想要索引0和2,pext
的控制掩码应为0b000'...'111'000'111
。 pext
将获取与选择器中的1位对齐的010
和000
索引组。所选组将打包到输出的低位,因此输出将为0b000'...'010'000
。 (即[ ... 2 0 ]
)
请参阅注释代码,了解如何从输入矢量蒙版生成0b111000111
的{{1}}输入。
现在我们和压缩LUT在同一条船上:打开最多8个打包索引。
当您将所有部分组合在一起时,总共有三pext
/ pext
个。我从我想要的东西倒退了,所以在这方面也可能最容易理解它。 (即从洗牌线开始,然后从那里向后工作。)
如果我们使用每个字节一个索引而不是打包的3位组,我们可以简化解包。由于我们有8个索引,因此只能使用64位代码。
见this and a 32bit-only version on the Godbolt Compiler Explorer。我使用了pdep
s,因此可以使用#ifdef
或-m64
进行最佳编译。 gcc浪费了一些指令,但是clang做了非常好的代码。
-m32
这编译为没有内存加载的代码,只有立即常量。 (参见godbolt链接以及32位版本。)
#include <stdint.h>
#include <immintrin.h>
// Uses 64bit pdep / pext to save a step in unpacking.
__m256 compress256(__m256 src, unsigned int mask /* from movmskps */)
{
uint64_t expanded_mask = _pdep_u64(mask, 0x0101010101010101); // unpack each bit to a byte
expanded_mask *= 0xFF; // mask |= mask<<1 | mask<<2 | ... | mask<<7;
// ABC... -> AAAAAAAABBBBBBBBCCCCCCCC...: replicate each bit to fill its byte
const uint64_t identity_indices = 0x0706050403020100; // the identity shuffle for vpermps, packed to one index per byte
uint64_t wanted_indices = _pext_u64(identity_indices, expanded_mask);
__m128i bytevec = _mm_cvtsi64_si128(wanted_indices);
__m256i shufmask = _mm256_cvtepu8_epi32(bytevec);
return _mm256_permutevar8x32_ps(src, shufmask);
}
因此,根据Agner Fog's numbers,这是6 uops(不计算常量,或内联时消失的零扩展mov)。在Intel Haswell上,它的16c延迟(vmovq为1,每个pdep / imul / pext / vpmovzx / vpermps为3)。没有指令级并行性。然而,在这不是循环携带依赖的一部分的循环中(就像我在Godbolt链接中包含的那个),瓶颈有希望只是吞吐量,同时保持多次迭代。 / p>
这可以管理每3个周期一个的吞吐量,在port1上为pdep / pext / imul设置瓶颈。当然,对于加载/存储和循环开销(包括compare,movmsk和popcnt),总uop吞吐量很容易成为问题。 (例如,我的godbolt链接中的过滤器循环是带有铿锵声的14个uop,带有 # clang 3.7.1 -std=gnu++14 -O3 -march=haswell
mov eax, edi # just to zero extend: goes away when inlining
movabs rcx, 72340172838076673 # The constants are hoisted after inlining into a loop
pdep rax, rax, rcx # ABC -> 0000000A0000000B....
imul rax, rax, 255 # 0000000A0000000B.. -> AAAAAAAABBBBBBBB..
movabs rcx, 506097522914230528
pext rax, rcx, rax
vmovq xmm1, rax
vpmovzxbd ymm1, xmm1 # 3c latency since this is lane-crossing
vpermps ymm0, ymm1, ymm0
ret
以便于阅读。如果我们需要,它可以维持每4c一次迭代,与前端保持同步幸运的是,但我认为clang未能解释-fno-unroll-loops
对其输出的错误依赖性,因此它会在popcnt
函数的延迟的3/5处出现瓶颈。)
gcc使用多个指令乘以0xFF,使用左移8和compress256
。这需要额外的sub
指令,但最终结果是乘以2的延迟。(Haswell在寄存器重命名阶段处理mov
且延迟为零。)
由于支持AVX2的所有硬件也支持BMI2,因此在没有BMI2的情况下为AVX2提供版本可能毫无意义。
如果你需要在很长的循环中执行此操作,如果初始缓存未命中在足够的迭代中分摊,而只需解压缩LUT条目的开销较低,则LUT可能是值得的。你仍然需要mov
,所以你可以弹出掩码并将其用作LUT索引,但是你保存了一个pdep / imul / pexp。
您可以使用我使用的相同整数序列解压缩LUT条目,但当LUT条目在内存中启动时,@ Froglegs的movmskps
/ set1()
/ vpsrlvd
可能更好并且首先不需要进入整数寄存器。 (32位广播负载在英特尔CPU上不需要ALU uop)。然而,Haswell的变量是3 uops(但Skylake只有1次)。
答案 1 :(得分:7)
如果你的目标是AMD Zen,这个方法可能更受欢迎,因为ryzen上的pdepand pext非常慢(每个18个周期)。
我提出了这种方法,它使用压缩的LUT,即768(+1填充)字节,而不是8k。它需要广播单个标量值,然后在每个通道中移动不同的量,然后屏蔽到低3位,这提供0-7 LUT。
这是内在函数版本,以及构建LUT的代码。
//Generate Move mask via: _mm256_movemask_ps(_mm256_castsi256_ps(mask)); etc
__m256i MoveMaskToIndices(u32 moveMask) {
u8 *adr = g_pack_left_table_u8x3 + moveMask * 3;
__m256i indices = _mm256_set1_epi32(*reinterpret_cast<u32*>(adr));//lower 24 bits has our LUT
// __m256i m = _mm256_sllv_epi32(indices, _mm256_setr_epi32(29, 26, 23, 20, 17, 14, 11, 8));
//now shift it right to get 3 bits at bottom
//__m256i shufmask = _mm256_srli_epi32(m, 29);
//Simplified version suggested by wim
//shift each lane so desired 3 bits are a bottom
//There is leftover data in the lane, but _mm256_permutevar8x32_ps only examines the first 3 bits so this is ok
__m256i shufmask = _mm256_srlv_epi32 (indices, _mm256_setr_epi32(0, 3, 6, 9, 12, 15, 18, 21));
return shufmask;
}
u32 get_nth_bits(int a) {
u32 out = 0;
int c = 0;
for (int i = 0; i < 8; ++i) {
auto set = (a >> i) & 1;
if (set) {
out |= (i << (c * 3));
c++;
}
}
return out;
}
u8 g_pack_left_table_u8x3[256 * 3 + 1];
void BuildPackMask() {
for (int i = 0; i < 256; ++i) {
*reinterpret_cast<u32*>(&g_pack_left_table_u8x3[i * 3]) = get_nth_bits(i);
}
}
这是由MSVC生成的程序集:
lea ecx, DWORD PTR [rcx+rcx*2]
lea rax, OFFSET FLAT:unsigned char * g_pack_left_table_u8x3 ; g_pack_left_table_u8x3
vpbroadcastd ymm0, DWORD PTR [rcx+rax]
vpsrlvd ymm0, ymm0, YMMWORD PTR __ymm@00000015000000120000000f0000000c00000009000000060000000300000000
答案 2 :(得分:7)
请参阅我对AVX2 + BMI2的其他答案,没有LUT。
由于您提到了对AVX512可扩展性的担忧:不用担心,确实存在AVX512F指令:
VCOMPRESSPS
— Store Sparse Packed Single-Precision Floating-Point Values into Dense Memory。 (还有double和32或64bit整数元素(vpcompressq
)的版本,但不是字节或字(16bit))。它类似于BMI2 pdep
/ pext
,但是对于向量元素而不是整数寄存器中的位。
目标可以是向量寄存器或内存操作数,而源是向量和掩码寄存器。使用寄存器dest,它可以将高位合并或归零。使用内存dest,&#34;只有连续的向量被写入目标内存位置&#34;。
要计算出下一个向量的指针前进距离,请弹出掩码。
我们假设你要过滤除数组中值&gt; = 0以外的所有内容:
#include <stdint.h>
#include <immintrin.h>
size_t filter_non_negative(float *__restrict__ dst, const float *__restrict__ src, size_t len) {
const float *endp = src+len;
float *dst_start = dst;
do {
__m512 sv = _mm512_loadu_ps(src);
__mmask16 keep = _mm512_cmp_ps_mask(sv, _mm512_setzero_ps(), _CMP_GE_OQ); // true for src >= 0.0, false for unordered and src < 0.0
_mm512_mask_compressstoreu_ps(dst, keep, sv); // clang is missing this intrinsic, which can't be emulated with a separate store
src += 16;
dst += _mm_popcnt_u64(keep); // popcnt_u64 instead of u32 helps gcc avoid a wasted movsx, but is potentially slower on some CPUs
} while (src < endp);
return dst - dst_start;
}
将(使用gcc4.9或更高版本)编译为(Godbolt Compiler Explorer):
# Output from gcc6.1, with -O3 -march=haswell -mavx512f. Same with other gcc versions
lea rcx, [rsi+rdx*4] # endp
mov rax, rdi
vpxord zmm1, zmm1, zmm1 # vpxor xmm1, xmm1,xmm1 would save a byte, using VEX instead of EVEX
.L2:
vmovups zmm0, ZMMWORD PTR [rsi]
add rsi, 64
vcmpps k1, zmm0, zmm1, 29 # AVX512 compares have mask regs as a destination
kmovw edx, k1 # There are some insns to add/or/and mask regs, but not popcnt
movzx edx, dx # gcc is dumb and doesn't know that kmovw already zero-extends to fill the destination.
vcompressps ZMMWORD PTR [rax]{k1}, zmm0
popcnt rdx, rdx
## movsx rdx, edx # with _popcnt_u32, gcc is dumb. No casting can get gcc to do anything but sign-extend. You'd expect (unsigned) would mov to zero-extend, but no.
lea rax, [rax+rdx*4] # dst += ...
cmp rcx, rsi
ja .L2
sub rax, rdi
sar rax, 2 # address math -> element count
ret
理论上,加载位图并将一个数组过滤到另一个数组的循环应该在SKX / CSLX上每3个时钟以1个向量运行,无论向量宽度如何,都在端口5上出现瓶颈。(kmovb/w/d/q k1, eax
在p5上运行根据IACA和http://uops.info/测试,vcompressps
进入内存是2p5 +商店。
@ZachB在评论中报告说,实际上,使用ZMM _mm512_mask_compressstoreu_ps
的循环比实际CSLX硬件上的_mm256_mask_compressstoreu_ps
略慢。(我不是确定这是否是一个microbenchmark,允许256位版本退出&#34; 512位向量模式&#34;并且时钟更高,或者如果有512位代码。)
我怀疑未对齐的商店正在损坏512位版本。 vcompressps
可能有效地执行屏蔽的256或512位向量存储,如果它跨越高速缓存行边界,那么它必须做额外的工作。由于输出指针通常不是16个元素的倍数,因此全行512位存储几乎总是未对齐。
由于某种原因,未对齐的512位存储可能比缓存行分割的256位存储更糟糕,并且更频繁地发生;我们已经知道其他东西的512位向量化似乎更符合对齐。这可能只是因为每次发生时都会耗尽分裂加载缓冲区,或者处理缓存行拆分的回退机制对于512位向量的效率可能较低。
将vcompressps
标记到一个寄存器中会很有意思,它有单独的全向量重叠存储。这可能是相同的uops,但是当它是一个单独的指令时,商店可以微融合。如果掩盖商店与重叠商店之间存在一些差异,这将揭示它。
下面评论中讨论的另一个想法是使用vpermt2ps
为对齐商店建立完整的向量。这个would be hard to do branchlessly和我们填充向量时的分支可能会错误预测,除非位掩码有一个非常规则的模式,或大量的全0和全-1。
通过正在构造的向量具有4或6个循环的循环承载链的无分支实现可能是可能的,使用vpermt2ps
和混合或其他东西来替换它时#&#39;&# 34;全&#34 ;.使用对齐的向量存储每次迭代,但仅在向量已满时移动输出指针。
这可能比当前Intel CPU上未对齐存储的vcompressps慢。
答案 3 :(得分:7)
将为@PeterCordes的一个很好的答案添加更多信息:https://stackoverflow.com/a/36951611/5021064。
我使用std::remove from C++ standard来实现整数类型。一旦可以进行压缩,该算法就相对简单:加载寄存器,压缩,存储。首先,我将展示各种变体,然后是基准。
最后,我对拟议的解决方案做了两个有意义的改动:
__m128i
使用_mm_shuffle_epi8
指令注册任何元素类型__m256i
寄存器,元素类型至少为4个字节,使用_mm256_permutevar8x32_epi32
当类型小于256位寄存器的4个字节时,我将它们拆分为两个128位寄存器,并分别压缩/存储每个。
链接到编译器资源管理器,您可以在其中看到完整的程序集(底部有using type
和width
(每包中的元素个数),您可以插入它们以获得不同的变化):{ {3}}
注意:我的代码在C ++ 17中,并且正在使用自定义simd包装器,因此我不知道它的可读性。如果您想阅读我的代码->其中大部分位于Godbolt顶部链接的后面。另外,所有代码都在https://gcc.godbolt.org/z/yQFR2t上。
两种情况下@PeterCordes答案的实现方式
注意:与掩码一起,我还使用popcount计算剩余的元素数。也许在某些情况下不需要它,但我还没有看到它。
掩盖_mm_shuffle_epi8
0xfedcba9876543210
__m128i
x << 4 | x & 0x0f0f
分散索引的示例。假设选择了第7个元素和第6个元素。
这意味着相应的缩写为:0x00fe
。在<< 4
和|
之后,我们将得到0x0ffe
。然后我们清除第二个f
。
完整的遮罩代码:
// helper namespace
namespace _compress_mask {
// mmask - result of `_mm_movemask_epi8`,
// `uint16_t` - there are at most 16 bits with values for __m128i.
inline std::pair<__m128i, std::uint8_t> mask128(std::uint16_t mmask) {
const std::uint64_t mmask_expanded = _pdep_u64(mmask, 0x1111111111111111) * 0xf;
const std::uint8_t offset =
static_cast<std::uint8_t>(_mm_popcnt_u32(mmask)); // To compute how many elements were selected
const std::uint64_t compressed_idxes =
_pext_u64(0xfedcba9876543210, mmask_expanded); // Do the @PeterCordes answer
const __m128i as_lower_8byte = _mm_cvtsi64_si128(compressed_idxes); // 0...0|compressed_indexes
const __m128i as_16bit = _mm_cvtepu8_epi16(as_lower_8byte); // From bytes to shorts over the whole register
const __m128i shift_by_4 = _mm_slli_epi16(as_16bit, 4); // x << 4
const __m128i combined = _mm_or_si128(shift_by_4, as_16bit); // | x
const __m128i filter = _mm_set1_epi16(0x0f0f); // 0x0f0f
const __m128i res = _mm_and_si128(combined, filter); // & 0x0f0f
return {res, offset};
}
} // namespace _compress_mask
template <typename T>
std::pair<__m128i, std::uint8_t> compress_mask_for_shuffle_epi8(std::uint32_t mmask) {
auto res = _compress_mask::mask128(mmask);
res.second /= sizeof(T); // bit count to element count
return res;
}
掩盖_mm256_permutevar8x32_epi32
这几乎是一对一的@PeterCordes解决方案-唯一的区别是_pdep_u64
位(他建议将此作为注释)。
我选择的遮罩为0x5555'5555'5555'5555
。这个想法是-我有32位的mmask,4位是8个整数。我要获取64位=>我需要将32位的每一位转换为2 =>因此0101b =5。乘数也从0xff更改为3,因为我将为每个整数而不是1获得0x55。 / p>
完整的遮罩代码:
// helper namespace
namespace _compress_mask {
// mmask - result of _mm256_movemask_epi8
inline std::pair<__m256i, std::uint8_t> mask256_epi32(std::uint32_t mmask) {
const std::uint64_t mmask_expanded = _pdep_u64(mmask, 0x5555'5555'5555'5555) * 3;
const std::uint8_t offset = static_cast<std::uint8_t(_mm_popcnt_u32(mmask)); // To compute how many elements were selected
const std::uint64_t compressed_idxes = _pext_u64(0x0706050403020100, mmask_expanded); // Do the @PeterCordes answer
// Every index was one byte => we need to make them into 4 bytes
const __m128i as_lower_8byte = _mm_cvtsi64_si128(compressed_idxes); // 0000|compressed indexes
const __m256i expanded = _mm256_cvtepu8_epi32(as_lower_8byte); // spread them out
return {expanded, offset};
}
} // namespace _compress_mask
template <typename T>
std::pair<__m256i, std::uint8_t> compress_mask_for_permutevar8x32(std::uint32_t mmask) {
static_assert(sizeof(T) >= 4); // You cannot permute shorts/chars with this.
auto res = _compress_mask::mask256_epi32(mmask);
res.second /= sizeof(T); // bit count to element count
return res;
}
基准
处理器:github(现代消费级CPU,不支持AVX-512)
编译器:clang,从版本10发行版附近的主干构建
编译器选项:--std=c++17 --stdlib=libc++ -g -Werror -Wall -Wextra -Wpedantic -O3 -march=native -mllvm -align-all-functions=7
微基准测试库:Intel Core i7 9700K
控制代码对齐:
如果您不熟悉此概念,请阅读google benchmark或观看this
基准二进制文件中的所有函数都与128字节边界对齐。每个基准测试函数被重复64次,并且在函数开始处(进入循环之前)使用不同的noop幻灯片。我显示的主要数字是每次测量的最小值。我认为这是可行的,因为该算法是内联的。我也得到了非常不同的结果,这也证实了我的观点。在答案的最底部,我展示了代码对齐的影响。
注意:this。 BENCH_DECL_ATTRIBUTES只是noinline
基准从数组中删除一些百分比的0。我测试了{0、5、20、50、80、95、100}百分比为零的数组。
我测试了3种大小:40个字节(以查看是否适用于非常小的数组),1000个字节和10000个字节。由于SIMD,我按大小分组,取决于数据的大小,而不是元素的数量。元素计数可以从元素大小得出(1000字节为1000个字符,但500个短裤和250个整数)。由于非SIMD代码花费的时间主要取决于元素数,因此对于char而言,获胜应该更大。
图:x-零百分比,y-时间(以纳秒为单位)。 padding:min表示所有比对中的最小值。
40个字节的数据(40个字符)
对于40个字节,这甚至对于char来说都没有意义-当我在非SIM码上使用128位寄存器时,我的实现会慢8-10倍。因此,例如,编译器应谨慎执行此操作。
1000个字节的数据(1000个字符)
显然,非SIMD版本受分支预测支配:当我们得到少量零时,我们得到的加速较小:没有0时-约为3倍,对于5%零-约为5-6倍。对于分支预测器无法帮助非SIMD版本的情况-大约可以提高27倍的速度。 simd代码的一个有趣特性是它的性能往往对数据的依赖性要小得多。使用128 vs 256寄存器几乎没有什么区别,因为大多数工作仍分为2 128个寄存器。
1000个字节的数据,500个短裤
与短裤类似的结果,只是增益小得多-最多可达2倍。 我不知道为什么对于非Simd代码,短裤比char做得好得多:我希望短裤能快两倍,因为只有500条短裤,但实际上相差最多10倍。
1000字节的数据,250整数
对于1000版本,只有256位版本才有意义-赢得20-30%的胜利(不包括0)以消除以往的错误(完美的分支预测,对于非Simd代码则不移除)。
价值10000字节的数据,10000个字符
与1000个字符相同的数量级获胜:当分支预测器帮助时,速度快2到6倍;不使用分支预测器时,速度快27倍。
相同的图,仅simd版本:
在这里我们可以看到,使用256位寄存器并将它们分成2 128位寄存器,大约有10%的优势:快10%。它的大小从88条指令增加到129条指令,数量不多,因此根据您的用例可能有意义。对于基线-非SIMD版本是79条指令(据我所知-这些指令比SIMD指令小)。
价值10,000字节的数据,5,000短裤
从20%到9倍获胜,具体取决于数据分布。没有显示256位和128位寄存器之间的比较-它几乎与chars相同,而对于256位寄存器,则赢得了大约10%的相同胜利。
价值10,000字节的数据,2,500整数
使用256位寄存器似乎很有意义,该版本比128位寄存器快约2倍。与非Simd代码进行比较时-从具有完美分支预测的20%获胜到没有分支预测的3.5-4倍。
结论:当您有足够的数据量(至少1000个字节)时,对于没有AVX-512的现代处理器而言,这可能是非常值得的优化
PS:
要删除的元素的百分比
一方面,过滤一半的元素并不常见。另一方面,在排序=>的过程中可以在分区中使用类似的算法,该算法实际上预计具有〜50%的分支选择。
代码对齐影响
问题是:如果代码恰好对齐不当,它有多少价值?
(通常来说-几乎无能为力)。
我只显示10'000字节。
这些图在每个百分点上都有用于最小和最大的两条线(这意味着-这不是最佳/最差的代码对齐方式-对于给定的百分比,这是最佳的代码对齐方式。)
代码对齐的影响-非模拟
从不良分支预测的15-20%到分支预测有很大帮助的2-3倍。 (分支预测变量已知会受到代码对齐的影响。)
由于某种原因-0%根本不受影响。可以通过std::remove
首先进行线性搜索来找到要删除的第一个元素来解释。显然,对短裤的线性搜索不受影响。
除此之外-价值从10%升至1.6-1.8倍
与短裤相同-不影响0。一旦我们拆下零件,它的价值便从1.3倍提高到5倍,然后达到最佳情况。
代码对齐影响-SIMD版本
不显示shorts和ints 128,因为它几乎与chars相同。
我们可以看到,对于算法的Simd版本,与非Simd版本相比,代码对齐的影响要小得多。我怀疑这实际上是由于没有分支。
答案 4 :(得分:6)
如果有人感兴趣,这里是SSE2的解决方案,它使用指令LUT而不是数据LUT即跳转表。对于AVX,这需要256个案例。
每当你在下面调用LeftPack_SSE2
时,它基本上使用三个指令:jmp,shufps,jmp。 16个案例中有5个不需要修改向量。
static inline __m128 LeftPack_SSE2(__m128 val, int mask) {
switch(mask) {
case 0:
case 1: return val;
case 2: return _mm_shuffle_ps(val,val,0x01);
case 3: return val;
case 4: return _mm_shuffle_ps(val,val,0x02);
case 5: return _mm_shuffle_ps(val,val,0x08);
case 6: return _mm_shuffle_ps(val,val,0x09);
case 7: return val;
case 8: return _mm_shuffle_ps(val,val,0x03);
case 9: return _mm_shuffle_ps(val,val,0x0c);
case 10: return _mm_shuffle_ps(val,val,0x0d);
case 11: return _mm_shuffle_ps(val,val,0x34);
case 12: return _mm_shuffle_ps(val,val,0x0e);
case 13: return _mm_shuffle_ps(val,val,0x38);
case 14: return _mm_shuffle_ps(val,val,0x39);
case 15: return val;
}
}
__m128 foo(__m128 val, __m128 maskv) {
int mask = _mm_movemask_ps(maskv);
return LeftPack_SSE2(val, mask);
}
答案 5 :(得分:0)
这可能有点晚了,尽管我最近遇到了这个确切的问题并找到了一个使用严格 AVX 实现的替代解决方案。如果您不关心解压缩的元素是否与每个向量的最后一个元素交换,这也可以工作。以下是AVX版本:
inline __m128 left_pack(__m128 val, __m128i mask) noexcept
{
const __m128i shiftMask0 = _mm_shuffle_epi32(mask, 0xA4);
const __m128i shiftMask1 = _mm_shuffle_epi32(mask, 0x54);
const __m128i shiftMask2 = _mm_shuffle_epi32(mask, 0x00);
__m128 v = val;
v = _mm_blendv_ps(_mm_permute_ps(v, 0xF9), v, shiftMask0);
v = _mm_blendv_ps(_mm_permute_ps(v, 0xF9), v, shiftMask1);
v = _mm_blendv_ps(_mm_permute_ps(v, 0xF9), v, shiftMask2);
return v;
}
本质上,val
中的每个元素都使用位域 0xF9
向左移动一次,以与其未移动的变体混合。接下来,根据输入掩码(其中第一个非零元素在其余元素 3 和 4 上广播)混合移位和未移位版本。再重复此过程两次,在每次迭代时将 mask
的第二个和第三个元素广播到其后续元素,这将提供 _pdep_u32()
BMI2 指令的 AVX 版本。
如果您没有 AVX,您可以轻松地将每个 _mm_permute_ps()
替换为 _mm_shuffle_ps()
,以获得与 SSE4.1 兼容的版本。
如果您使用双精度,这里有一个适用于 AVX2 的附加版本:
inline __m256 left_pack(__m256d val, __m256i mask) noexcept
{
const __m256i shiftMask0 = _mm256_permute4x64_epi64(mask, 0xA4);
const __m256i shiftMask1 = _mm256_permute4x64_epi64(mask, 0x54);
const __m256i shiftMask2 = _mm256_permute4x64_epi64(mask, 0x00);
__m256d v = val;
v = _mm256_blendv_pd(_mm256_permute4x64_pd(v, 0xF9), v, shiftMask0);
v = _mm256_blendv_pd(_mm256_permute4x64_pd(v, 0xF9), v, shiftMask1);
v = _mm256_blendv_pd(_mm256_permute4x64_pd(v, 0xF9), v, shiftMask2);
return v;
}
另外,_mm_popcount_u32(_mm_movemask_ps(val))
可用于确定左包装后剩余的元素数。