SSE mov指令可以跳过每个第2个字节?

时间:2016-09-18 16:01:33

标签: assembly 64-bit sse2 sse sse4

我需要将所有奇数字节从一个存储位置复制到另一个存储位置。即复制第一,第三,第五等。具体来说,我是从包含2000个字符/属性字的文本区0xB8000复制的。我想跳过属性字节,最后得到字符。以下代码工作正常:

      mov eax, ecx                       ; eax = number of bytes (1 to 2000)
      mov rsi, rdi                       ; rsi = source
      mov rdi, CMD_BLOCK                 ; rdi = destination
@@:   movsb                              ; copy 1 byte
      inc rsi                            ; skip the next source byte
      dec eax
      jnz @b    

要复制的数字或字符是1到2000.我最近开始使用sse2,sse3 sse4.2,但找不到可以减少循环的指令。理想情况下,我希望将循环从2000减少到250,如果有一条指令可以跳过每个第二个字节,一次加载128位后,这将是可能的。

3 个答案:

答案 0 :(得分:2)

我会做这样的事情,处理32个输入字节到16个输出字节 循环迭代:

const __m128i vmask = _mm_set1_epi16(0x00ff);

for (i = 0; i < n; i += 16)
{
    __m128i v0 = _mm_loadu_si128(&a[2 * i]);      // load 2 x 16 input bytes (MOVDQU)
    __m128i v1 = _mm_loadu_si128(&a[2 * i + 16]);
    v0 = _mm_and_si128(v0, vmask);                // mask unwanted bytes     (PAND)
    v1 = _mm_and_si128(v1, vmask);
    __m128 v = _mm_packus_epi16(v0, v1);          // pack low bytes          (PACKUSWB)
    _mm_storeu_si128(v, &b[i];                    // store 16 output bytes   (MOVDQU)
}

这当然是带有内在函数的C - 如果你真的想在汇编程序中这样做,那么你可以将上面的每个内在函数转换成相应的指令。

答案 1 :(得分:2)

我根本不会使用SIMD指令。我怀疑你可以大大超过64位负载的性能,因为视频内存未被缓存,并且总线不太可能支持更广泛的事务。

我使用这样的东西:

     lea rdi, [rdi + rcx * 2 - 8]
loop:
     mov rax, [rdi]
     mov [CMD_BLOCK + rcx - 4], al
     shr rax, 16
     mov [CMD_BLOCK + rcx - 4 + 1], al
     shr rax, 16
     mov [CMD_BLOCK + rcx - 4 + 2], al
     shr rax, 16
     mov [CMD_BLOCK + rcx - 4 + 3], al
     sub rdi, 8
     sub rcx, 4
     jnz loop

看起来效率低下,但由于负载(mov rax,[rdi])上有一个巨大的停顿,所以其他一切都可能与此并行发生。

或者在C:

void copy_text(void *dest, void *src, int len) {
    unsigned long long *sp = src;
    unsigned char *dp = dest;
    int i;

    for(i = 0; i < len; i += 4) {
        unsigned long long a = *sp++;
        *dp++ = (unsigned char) a;
        a >>= 16;
        *dp++ = (unsigned char) a;
        a >>= 16;
        *dp++ = (unsigned char) a;
        a >>= 16;
        *dp++ = (unsigned char) a;
    }
}      

无论您做什么,代码的性能都将取决于未缓存的视频内存读取的成本。这真的是您需要优化的唯一部分。

此外,如果您正在执行大量这些读取操作,因此代码的性能确实很重要,您应该看看是否可以在正常的缓存内存中保留文本的副本。视频内存不是为了读取而设计的,所以这应该是最后的选择。 (或者如果您在Linux内核中运行此代码或其他内容,请查看您是否已经可以访问正常内存中的副本。)

答案 2 :(得分:2)

您是否真的在x86-64模式下在VGA文本模式视频内存上使用SIMD?这很有趣,但在现实生活中实际上是合理的,并且作为一些SIMD数据操作的用例。

但是,如果您真的从视频内存中读取,那么您可能正在进行未缓存的加载,这很糟糕,这意味着您应该重新设计系统,这样您就不必这样做了。 (见罗斯的建议答案)

在USWC视频内存上,您可以从MOVNTDQA获得大幅提升。请参阅Intel's article,以及我关于NT加载的几个答案:here,尤其是this one,其中我解释了x86 ISA手册中关于NT加载不会覆盖内存排序语义的内容,所以他们& #39;除非你在弱有序的记忆区域使用它们,否则不会被弱化。

如您所料,您在SIMD指令集中找不到复制指令;你必须自己在加载和存储之间的寄存器中进行数据处理。甚至没有一条SSE / AVX指令会为您执行此操作。 (ARM NEON的unzip instruction确实解决了整个问题。)

你应该使用SSE2 PACKUSWB,将(带符号)int16_t的两个向量打包到uint8_t的一个向量中。将每个字元素的高位字节归零后,饱和到0..255根本不会修改数据。

这里是一个真实的(未经测试的)循环,它对齐源指针以最大限度地减少跨越缓存行边界的惩罚,并使用一些寻址模式技巧来保存循环中的指令。 / p>

未对齐的负载对Nehalem的影响非常小,后来,当他们越过缓存线边界时,大多只是额外的延迟。因此,如果您想从视频内存使用NT加载,这将非常有用。或者,如果您在大型副本结束时读取超出src的末尾,这可能很有用。

我们的负载是商店的两倍,因此如果加载/存储吞吐量是一个问题,那么对齐的负载(而不是对齐的存储)可能是最佳的。但是,有太多的ALU工作来使缓存加载/存储吞吐量饱和,因此保持简单的未对齐加载(如Paul R&#s;循环)应该在大多数CPU和用例上都能很好地工作

  mov       edx, CMD_BUFFER    ; or RIP-relative LEA, or hopefully this isn't even static in the first place and this instruction is something else

  ;; rdi = source   ; yes this is "backwards", but if you already have the src pointer in rdi, don't waste instructions
  ;; rcx = count
  ;; rdx = dest

  pcmpeqw   xmm7, xmm7         ; all ones (0xFF repeating)
  psrlw     xmm7, 8            ; 0x00FF repeating: mask for zeroing the high bytes

  ;cmp       ecx, 16
  ;jb        fallback_loop     ; just make CMD_BUFFER big enough that it's ok to copy 16 bytes when you only wanted 1.  Assuming the src is also padded at the end so you can read without faulting.

  ;; First potentially-unaligned 32B of source data
  ;; After this, we only read 32B chunks of 32B-aligned source that contain at least one valid byte, and thus can't segfault at the end.
  movdqu    xmm0, [rdi]             ; only diff from loop body: addressing mode and unaligned loads
  movdqu    xmm1, [rdi + 16]
  pand      xmm0, xmm7
  pand      xmm1, xmm7
  packuswb  xmm0, xmm1
  movdqu    [rdx], xmm0

  ;; advance pointers just to the next src alignment boundary.  src may have different alignment than dst, so we can't just AND both of them
  ;; We can only use aligned loads for the src if it was at least word-aligned on entry, but that should be safe to assume.
  ;; There's probably a way to do this in fewer instructions.
  mov       eax, edi
  add       rdi, 32                ; advance 32B
  and       rdi, -32               ; and round back to an alignment boundary
  sub       eax, edi               ; how far rdi actually advanced
  shr       eax, 1
  add       rdx, rax               ; advance dst by half that.

  ;; if rdi was aligned on entry, the it advances by 32 and rdx advances by 16.  If it's guaranteed to always be aligned by 32, then simplify the code by removing this peeled unaligned iteration!
  ;; if not, the first aligned loop iteration will overlap some of the unaligned loads/store, but that's fine.

  ;; TODO: fold the above calculations into this other loop setup

  lea       rax, [rdx + rdx]
  sub       rdi, rax           ; source = [rdi + 2*rdx], so we can just increment our dst pointer.

  lea       rax, [rdx + rcx]   ; rax = end pointer.  Assumes ecx was already zero-extended to 64-bit



  ; jmp      .loop_entry       ; another way to check if we're already done
  ; Without it, we don't check for loop exit until we've already copied 64B of input to 32B of output.
  ; If small inputs are common, checking after the first unaligned vectors does make sense, unless leaving it out makes the branch more predictable.  (All sizes up to 32B have identical branch-not-taken behaviour).

ALIGN 16
.pack_loop:

  ; Use SSE4.1  movntdqa  if reading from video RAM or other UCSW memory region
  movdqa    xmm0, [rdi + 2*rdx]         ; indexed addressing mode is ok: doesn't need to micro-fuse because loads are already a single uop
  movdqa    xmm1, [rdi + 2*rdx + 16]    ; these could optionally be movntdqa loads, since we got any unaligned source data out of the way.
  pand      xmm0, xmm7
  pand      xmm1, xmm7
  packuswb  xmm0, xmm1
  movdqa    [rdx], xmm0        ; non-indexed addressing mode: can micro-fuse
  add       rdx, 16
.loop_entry:
  cmp       rdx, rax
  jb        .pack_loop         ; exactly 8 uops: should run at 1 iteration per 2 clocks

  ;; copies up to 15 bytes beyond the requested amount, depending on source alignment.

  ret

使用AVX的非破坏性第3操作数编码,可以将负载折叠到PAND(vpand xmm0, xmm7, [rdi + 2*rdx])中。但indexed addressing modes can't micro-fuse on at least some SnB-family CPUs,您可能希望展开add rdi, 32以及add rdx, 16,而不是使用相对于目的地寻址来源的技巧。

AVX会将循环体降低到4个融合域uop,用于2xload +和/ pack / store,以及循环开销。通过展开,我们可以开始接近英特尔Haswell的理论最大吞吐量2个负载+每个时钟1个存储(虽然它无法维持这一点;存储地址uops将窃取p23周期而不是有时使用p7。假设所有L1缓存命中率低于96B峰值吞吐量,英特尔优化手册提供了实际可持续吞吐量数量,例如每时钟加载和存储的~84B(使用32字节向量)。)

您还可以使用字节shuffle(SSSE3 PSHUFB)来获取打包到低64位的向量的偶数字节。 (然后为每个128位负载执行单个64位MOVQ存储,或者将两个下半部分与PUNPCKLQDQ组合)。但这很糟糕,因为(每128位源数据向量),它有2个shuffles + 2个商店,或3个shuffles + 1个商店。你可以通过使用不同的shuffle mask来降低合并成本,例如:将偶数字节混合到一个向量的低半部分,以及另一个向量的上半部分。由于PSHUFB也可以免费归零任何字节,因此可以与POR结合使用(而不是稍微昂贵的PBLENDW或AVX2 VPBLENDD)。这是2个shuffles + 1个boolean + 1个商店,仍然是洗牌的瓶颈。

PACKUSWB方法是2个布尔操作+ 1个shuffle + 1个存储(因为PAND可以在更多执行端口上运行,所以更少的瓶颈;例如每个时钟3个,而每个时钟1个用于混洗)。

AVX512BW (可在Skylake-avx512 but not on KNL上提供)提供
VPMOVWB ymm1/m256 {k1}{z}, zmm2__m256i _mm512_cvtepi16_epi8 (__m512i a)),其中包含截断而不是饱和。与SSE包指令不同,它只需要1个输入并产生更窄的结果(可以是存储器目的地)。 (vpmovswbvpmovuswb相似,并使用有符号或无符号饱和度打包。所有与pmovzx相同大小的组合都可用,例如vpmovqb xmm1/m64 {k1}{z}, zmm2,因此您不能使用{{1}}。需要多个步骤.Q和D源尺寸在AVX512F中。

使用C / C ++内在函数甚至可以公开memory-dest功能,从而可以方便地用C编写一个带掩码的存储。(这是pmovzx where it's inconvenient to use intrinsics and get the compiler to emit a pmovzx load的一个很好的改变)。

AVX512VBMI (预计在英特尔Cannonlake中)可以使用一个VPERMT2B 对一个512b输出执行两个输入,给定一个带有偶数字节的随机屏蔽两个输入向量并产生一个结果向量。

如果VPERM2TB慢于VPMOVWB,则一次使用VPMOVWB作为一个向量可能是最佳的。即使它们具有相同的吞吐量/延迟/ uop计数,增益也可能很小,以至于不值得制作另一个版本并检测AVX512VBMI而不是AVX512BW。 (如果没有AVX512BW,CPU不可能拥有AVX512VBMI,尽管这是可能的。)