AVX2基于面具打包的最有效方法是什么?

时间:2016-04-29 07:30:11

标签: c++ vectorization sse simd avx2

如果你有一个输入数组和一个输出数组,但是你只想写那些通过某种条件的元素,那么在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做这个的任何技巧?或者最有效的方法是什么?

以下是上述文件中左包装问题的说明:

Left.Packing.Problem

由于

6 个答案:

答案 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,使用pshufbvpermilps 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'111pext将获取与选择器中的1位对齐的010000索引组。所选组将打包到输出的低位,因此输出将为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

性能:Skylake-X / Cascade Lake上的256位向量可能更快

理论上,加载位图并将一个数组过滤到另一个数组的循环应该在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来实现整数类型。一旦可以进行压缩,该算法就相对简单:加载寄存器,压缩,存储。首先,我将展示各种变体,然后是基准。

最后,我对拟议的解决方案做了两个有意义的改动:

  1. __m128i使用_mm_shuffle_epi8指令注册任何元素类型
  2. __m256i寄存器,元素类型至少为4个字节,使用_mm256_permutevar8x32_epi32

当类型小于256位寄存器的4个字节时,我将它们拆分为两个128位寄存器,并分别压缩/存储每个。

链接到编译器资源管理器,您可以在其中看到完整的程序集(底部有using typewidth(每包中的元素个数),您可以插入它们以获得不同的变化):{ {3}}

注意:我的代码在C ++ 17中,并且正在使用自定义simd包装器,因此我不知道它的可读性。如果您想阅读我的代码->其中大部分位于Godbolt顶部链接的后面。另外,所有代码都在https://gcc.godbolt.org/z/yQFR2t上。

两种情况下@PeterCordes答案的实现方式

注意:与掩码一起,我还使用popcount计算剩余的元素数。也许在某些情况下不需要它,但我还没有看到它。

掩盖_mm_shuffle_epi8

  1. 将每个字节的索引写成一个半字节:0xfedcba9876543210
  2. 将成对的索引分为8条短裤,并打包成__m128i
  3. 使用x << 4 | x & 0x0f0f
  4. 传播它们

分散索引的示例。假设选择了第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个字符)

benchmarking code

对于40个字节,这甚至对于char来说都没有意义-当我在非SIM码上使用128位寄存器时,我的实现会慢8-10倍。因此,例如,编译器应谨慎执行此操作。

1000个字节的数据(1000个字符)

40 bytes, chars

显然,非SIMD版本受分支预测支配:当我们得到少量零时,我们得到的加速较小:没有0时-约为3倍,对于5%零-约为5-6倍。对于分支预测器无法帮助非SIMD版本的情况-大约可以提高27倍的速度。 simd代码的一个有趣特性是它的性能往往对数据的依赖性要小得多。使用128 vs 256寄存器几乎没有什么区别,因为大多数工作仍分为2 128个寄存器。

1000个字节的数据,500个短裤

1000 chars

与短裤类似的结果,只是增益小得多-最多可达2倍。 我不知道为什么对于非Simd代码,短裤比char做得好得多:我希望短裤能快两倍,因为只有500条短裤,但实际上相差最多10倍。

1000字节的数据,250整数

1000 bytes, shorts

对于1000版本,只有256位版本才有意义-赢得20-30%的胜利(不包括0)以消除以往的错误(完美的分支预测,对于非Simd代码则不移除)。

价值10000字节的数据,10000个字符

1000 bytes, ints

与1000个字符相同的数量级获胜:当分支预测器帮助时,速度快2到6倍;不使用分支预测器时,速度快27倍。

相同的图,仅simd版本:

10'000 bytes, chars

在这里我们可以看到,使用256位寄存器并将它们分成2 128位寄存器,大约有10%的优势:快10%。它的大小从88条指令增加到129条指令,数量不多,因此根据您的用例可能有意义。对于基线-非SIMD版本是79条指令(据我所知-这些指令比SIMD指令小)。

价值10,000字节的数据,5,000短裤

10'000 chars, no non-simd

从20%到9倍获胜,具体取决于数据分布。没有显示256位和128位寄存器之间的比较-它几乎与chars相同,而对于256位寄存器,则赢得了大约10%的相同胜利。

价值10,000字节的数据,2,500整数

10'000 bytes, shorts

使用256位寄存器似乎很有意义,该版本比128位寄存器快约2倍。与非Simd代码进行比较时-从具有完美分支预测的20%获胜到没有分支预测的3.5-4倍。

结论:当您有足够的数据量(至少1000个字节)时,对于没有AVX-512的现代处理器而言,这可能是非常值得的优化

PS:

要删除的元素的百分比

一方面,过滤一半的元素并不常见。另一方面,在排序=>的过程中可以在分区中使用类似的算法,该算法实际上预计具有〜50%的分支选择。

代码对齐影响

问题是:如果代码恰好对齐不当,它有多少价值? (通常来说-几乎无能为力)。
我只显示10'000字节。
这些图在每个百分点上都有用于最小和最大的两条线(这意味着-这不是最佳/最差的代码对齐方式-对于给定的百分比,这是最佳的代码对齐方式。)

代码对齐的影响-非模拟

字符: 10'000 bytes, ints

从不良分支预测的15-20%到分支预测有很大帮助的2-3倍。 (分支预测变量已知会受到代码对齐的影响。)

短裤: code alignment, chars

由于某种原因-0%根本不受影响。可以通过std::remove首先进行线性搜索来找到要删除的第一个元素来解释。显然,对短裤的线性搜索不受影响。 除此之外-价值从10%升至1.6-1.8倍

整数: code alignment, shorts

与短裤相同-不影响0。一旦我们拆下零件,它的价值便从1.3倍提高到5倍,然后达到最佳情况。

代码对齐影响-SIMD版本

不显示shorts和ints 128,因为它几乎与chars相同。

字符-128位寄存器 code alignment, ints 慢约1.2倍

字符-256位寄存器 code alignment, chars - 128 慢约1.1-1.24倍

Ints-256位寄存器 code alignment, chars - 256 慢1.25-1.35倍

我们可以看到,对于算法的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)) 可用于确定左包装后剩余的元素数。