为什么其中一个比另一个快那么多?

9
我正在编写C++代码来查找内存中第一个不是0xFF的字节。为了利用bitscanforward,我编写了一段喜欢的内联汇编代码。但出于“可读性”以及未来证明(即SIMD向量化)的考虑,我想给g ++优化器一个机会。g ++没有进行向量化,但几乎得到了与我的非SIMD解决方案相同的结果。但由于某种原因,它的版本运行速度要慢得多,慢260000倍(即我必须将我的版本循环260,000次才能达到相同的执行时间)。我预计会有一些区别,但没想到差别那么大!有人能指出可能的原因吗?我只是想知道这样做是为了避免在未来的内联汇编代码中犯错误。

以下是C ++起点(在计数精度方面,这个代码存在一个错误,但为了进行此速度测试,我已将其简化):

uint64_t count3 (const void *data, uint64_t const &nBytes) {
      uint64_t count = 0;
      uint64_t block;
      do {
         block = *(uint64_t*)(data+count);
         if ( block != (uint64_t)-1 ) {
/*       count += __builtin_ctz(~block);   ignore this for speed test*/
            goto done;
          };
        count += sizeof(block);
      } while ( count < nBytes );
done:
      return (count>nBytes ? nBytes : count);
}

g++生成的汇编代码如下:

_Z6count3PKvRKm:
.LFB33:
    .cfi_startproc
    mov rdx, QWORD PTR [rsi]
    xor eax, eax
    jmp .L19
    .p2align 4,,10
    .p2align 3
.L21:
    add rax, 8
    cmp rax, rdx
    jnb .L18
.L19:
    cmp QWORD PTR [rdi+rax], -1
    je  .L21
.L18:
    cmp rax, rdx
    cmova   rax, rdx
    ret
    .cfi_endproc

我的内联汇编代码是

_Z6count2PKvRKm:
.LFB32:
    .cfi_startproc
    push    rbx
    .cfi_def_cfa_offset 16
    .cfi_offset 3, -16
    mov rbx, QWORD PTR [rsi]

    # count trailing bytes of 0xFF 
    xor     rax, rax  
.ctxff_loop_69:          
    mov     r9,  QWORD PTR [rdi+rax] 
    xor     r9, -1          
    jnz   .ctxff_final_69    
    add     rax, 8     
    cmp     rax, rbx 
    jl    .ctxff_loop_69    
.ctxff_final_69:         
    cmp     rax,rbx  
    cmova   rax,rbx  
    pop rbx
    .cfi_def_cfa_offset 8
    ret
    .cfi_endproc

据我所见,除了它比较数据字节和0xFF的方法不同外,其实质上是相同的。但我不认为这会导致计算时间上的很大差异。
虽然我的测试方法可能会导致错误,但我只是在下面简单的for循环中更改函数名和迭代长度:
(当N为1<<20时,并且a的所有字节除了最后一个字节都为0xFF)
测试1
   for (uint64_t i=0; i < ((uint64_t)1<<15); i++) {
      n = count3(a,N);
   }

测试 2

   for (uint64_t i=0; i < ((uint64_t)1<<33); i++) {
      n = count2(a,N);
   }

编辑:

这里是我的真实的带有SSE count1(), x64-64 count()以及平常的c++版本count0()count3() 的内联汇编代码。我开始尝试进入这个领域是因为我希望g++可以将我的count0()自动转化成count1()或者count2()。但很遗憾,它什么也没做,完全没有优化。我需要补充一下的是,我的平台没有AVX2,那就是我希望g++可以自动矢量化,这样代码可以在我更新平台时自动更新。

关于内联汇编中显式使用寄存器,如果我不显式地指定,g++会重复使用相同的寄存器来存储nBytescount

至于速度提升,我发现在XMM和QWORD之间的真正好处只是“循环展开”的效果,我在count2()中也实现了这个效果。

uint32_t count0(const uint8_t *data, uint64_t const &nBytes) {

  for (int i=0; i<nBytes; i++)
    if (data[i] != 0xFF) return i;

  return nBytes;
}
uint32_t count1(const void *data, uint64_t const &nBytes) {
  uint64_t count;
  __asm__("# count trailing bytes of 0xFF \n"
    "   xor     %[count], %[count]  \n"
    " vpcmpeqb  xmm0, xmm0, xmm0  \n" // make array of 0xFF

    ".ctxff_next_block_%=:        \n"
    " vpcmpeqb  xmm1, xmm0, XMMWORD PTR [%[data]+%[count]]  \n"
    " vpmovmskb r9, xmm1         \n"
    " xor     r9, 0xFFFF       \n" // test if all match (bonus negate r9)
    " jnz   .ctxff_tzc_%=        \n" // if !=0, STOP & tzcnt negated r9
    " add     %[count], 16       \n" // else inc
    " cmp     %[count], %[nBytes] \n"
    " jl    .ctxff_next_block_%=  \n" // while count < nBytes, loop
    " jmp   .ctxff_done_%=      \n" // else done + ALL bytes were 0xFF

    ".ctxff_tzc_%=:           \n"
    " tzcnt   r9, r9          \n" // count bytes up to non-0xFF
    " add     %[count], r9    \n"

    ".ctxff_done_%=:          \n" // more than 'nBytes' could be tested,
    " cmp     %[count],%[nBytes]  \n" // find minimum
    " cmova   %[count],%[nBytes]  "
    : [count] "=a" (count)
    : [nBytes] "b" (nBytes), [data] "d" (data)
    : "r9", "xmm0", "xmm1"
  );
  return count;
};

uint64_t count2 (const void *data, uint64_t const &nBytes) {
    uint64_t count;
  __asm__("# count trailing bytes of 0xFF \n"
    "    xor     %[count], %[count]  \n"

    ".ctxff_loop_%=:          \n"
    "    mov     r9,  QWORD PTR [%[data]+%[count]] \n"
    "    xor     r9, -1          \n" 
    "    jnz   .ctxff_final_%=    \n"
    "    add     %[count], 8     \n" 
    "    mov     r9,  QWORD PTR [%[data]+%[count]] \n"  // <--loop-unroll
    "    xor     r9, -1          \n" 
    "    jnz   .ctxff_final_%=    \n"
    "    add     %[count], 8     \n" 
    "    cmp     %[count], %[nBytes] \n"
    "    jl    .ctxff_loop_%=    \n"
    "    jmp   .ctxff_done_%=   \n" 

    ".ctxff_final_%=:            \n"
    "    bsf   r9,  r9           \n" // do tz count on r9 (either of first QWORD bits or XMM bytes)
    "    shr     r9,  3          \n" // scale BSF count accordiningly
    "    add     %[count], r9    \n"
    ".ctxff_done_%=:          \n" // more than 'nBytes' bytes could have been tested,
    "    cmp     %[count],%[nBytes]  \n" // find minimum of count and nBytes
    "    cmova   %[count],%[nBytes]  "
    : [count] "=a" (count)
    : [nBytes] "b" (nBytes), [data] "D" (data)
    : "r9"
  );
  return count;
}

inline static uint32_t tzcount(uint64_t const &qword) {
  uint64_t tzc;
  asm("tzcnt %0, %1" : "=r" (tzc) : "r" (qword) );
  return tzc;
};

uint64_t count3 (const void *data, uint64_t const &nBytes) {
      uint64_t count = 0;
      uint64_t block;
      do {
        block = *(uint64_t*)(data+count);
         if ( block != (uint64_t)-1 ) {
           count += tzcount(~block);
            goto done;
          };
        count += sizeof(block);
      } while ( count < nBytes );
done:
      return (count>nBytes ? nBytes : count);
}

uint32_t N = 1<<20;

int main(int argc, char **argv) {

  unsigned char a[N];
  __builtin_memset(a,0xFF,N);

  uint64_t n = 0, j;
   for (uint64_t i=0; i < ((uint64_t)1<<18); i++) {
      n += count2(a,N);
   }

 printf("\n\n %x %x %x\n",N, n, 0);   
  return n;
}

5
速度差260000倍几乎可以确定更快的测试实际上并没有做任何工作。在任何一个循环中都没有东西能解释这么大的速度差异。(也许自修改代码可以运行得那么慢,或者像Douglas说的,分页到磁盘上。)你确定260k次重复的循环实际上正在重复全部工作,而不仅仅是一个几乎为空的循环只做一些add之类的操作吗?肯定有某种测量误差。请发布完整的代码。 - Peter Cordes
另外,请注意 xor r9, -1not r9 是相同的,两者都无法与 jnz 宏融合。最好的方法是在循环外部使用 mov reg,-1,然后在循环内部使用 cmp reg,[mem]。这将使您的 cmp 可以在 Intel SnB 系列 CPU 上与 jcc 宏融合,而使用立即数和内存操作数时则不可能。(请参见 http://agner.org/optimize/)。此外,您可以使用 break 而不是 goto done - Peter Cordes
1
由于SSE2是x86-64的基线,您可以并且应该使用它。使用pcmpeqw xmm1,xmm1获取全0xFF的向量,并使用pcmpeqb与其进行比较。使用psubb对比较结果求和(减去-1),偶尔使用psadbw将字节水平求和为16位字,以避免溢出。非匹配计数=总字节数-匹配计数。或者,要在找到第一个匹配项时中断,请在比较结果上使用pmovmskb eax,xmm0,并使用cmp eax,0xFFFF / jne仅在所有16个向量元素都相等时才跳过。 - Peter Cordes
使用展开技术,标量版本可能每个时钟周期可以实现两次加载,包括比较和分支。向量版本需要每个比较和分支四个微操作,或每个比较和计数三个微操作。使用AVX1进行非破坏性操作的128b xmm向量,每个操作可以减少一个微操作(可以在不破坏全为1的向量的情况下进行比较和加载)。因此,对于比较和计数,您应该能够饱和SnB系列CPU上的加载单元,每个时钟周期加载2x 128b,但标量版本最多只能每个时钟周期执行2x 64b。 - Peter Cordes
@PeterCordes 我不是在计算所有的0xFF,只计算到第一个非0xFF。此外,我对数据有一些了解,0xFF最有可能出现在大块中,而不是单个字节中。bsf只是我的疏忽,但在这一点上已经无关紧要,因为:1)我的CPU实际上不支持真正的tzcnt,2)bsf仅在最后发生一次。我的CPU是i5-2550K。 - codechimp
显示剩余9条评论
2个回答

6

问题标题的答案

现在您已经发布了完整的代码:main函数中对count2(a,N)的调用被提升到循环外部。运行时间仍然随着循环次数略微增加(例如1<<18),但是该循环只执行单个add操作。编译器将其优化为类似于以下源代码:

uint64_t hoisted_count = count2(a,N);
for (uint64_t i=0; i < ((uint64_t)1<<18); i++) {
   n += hoisted_count;   // doesn't optimize to a multiply
}

没有寄存器冲突:%rax 保存了从 count2 内联的汇编语句的结果。然后它作为源操作数在小循环中使用,通过重复加法将其乘以 n

请查看 Godbolt Compiler Explorer 上的汇编代码,并注意关于 void* 类型算术运算的所有编译器警告:clang 拒绝编译您的代码。
## the for() loop in main, when using count2()
.L23:
    addq    %rax, %r12
    subq    $1, %rdx
    jne     .L23

%rdx 在这里是循环计数器,%r12 是累加器,保存了 n。不知道为什么 GCC 没有将其优化为常数时间乘法。

可以推测,运行速度慢 260k 倍的版本没有将整个 count2 移出循环。从 GCC 的角度来看,内联汇编版本要简单得多:汇编语句被视为其输入的纯函数,GCC 甚至不知道它会接触到内存。C 版本接触了大量内存,更难以证明它可以被移出循环。

在汇编语句中使用 "memory" 污点确实防止了它被提升,我在 godbolt 上检查过。你可以从向量块之前的分支目标的存在或缺失中看出来。

但无论如何,运行时间大约为 n + rep_count vs. n * rep_count

asm语句没有使用"memory"破坏或任何内存输入来告诉gcc它读取了由输入指针指向的内存。可能会发生不正确的优化,例如被提升出修改数组元素的循环。(请参见手册中的Clobbers section,其中有一个示例,使用匿名的struct内存输入代替全局的"memory"破坏。不幸的是,当内存块没有编译时常量大小时,我认为这是不能使用的。)

我认为-fno-inline可以防止提升,因为该函数未标记为__attribute__((const))或稍弱的__attribute__((pure))以表明没有副作用。在内联后,优化器可以看到asm语句。

count0 没有被优化成更好的东西,因为 gcc 和 clang 不能自动向量化循环,其中迭代次数在开始时未知。也就是说,它们对于像 strlenmemchr 这样的东西或者搜索循环一般都不擅长,即使它们被告知可以安全地访问超出搜索循环退出点结束处内存的内容(例如使用 char buf[static 512] 作为函数参数)。


针对汇编代码的优化:

如我在问题中所评论的那样,与cmp reg, 0xFFFF / jnz相比,使用xor reg, 0xFFFF / jnz是愚蠢的,因为cmp / jcc 可以宏观融合成一个比较和分支uop。 cmp reg, mem / jne 也可以宏观融合,因此标量版本执行load/xor/branch的操作每次比较使用了3倍的uops。 (当然,Sandybridge只能微观融合加载操作,如果不使用索引寻址模式。另外,SnB每个解码块只能宏观融合一对指令,但您可能会得到第一个cmp/jcc和循环分支进行宏观融合)。总之,xor是不好的想法。更好的方法是仅在tzcnt之前进行xor,因为在循环中节省uops比代码大小或总uops更重要。

你的标量循环包含9个融合域uop,这超过了每2个时钟周期迭代一次的单次发行限制。(SnB有4个宽度的流水线,对于微小的循环实际上可以维持这种速度。)
第一个问题中的代码缩进,count += __builtin_ctzif在同一级别,让我认为您正在计算不匹配的块,而不仅仅是找到第一个块。
不幸的是,我为第一个版本写的汇编代码没有解决与OP更新和更清晰的代码相同的问题。请参见此答案的旧版本,其中使用pcmpeqb / paddb计算0xFF字节,并使用psadbw进行水平求和以避免环绕。

使用SSE2(或AVX)来加速:

如果我们在pcmpeq的结果上进行分支,则比在cmp上进行分支需要更多的uops。如果我们的搜索数组很大,我们可以使用一个循环一次测试多个向量,然后在跳出循环后找到我们要查询的字节。

这种优化同样适用于AVX2。

以下是我的尝试,使用GNU C内联汇编和-masm=intel语法。(内置函数可能会获得更好的结果,特别是在内联时,因为编译器理解内置函数,因此可以通过它们进行常量传播等操作。然而,如果您了解目标微体系结构以及权衡,手写汇编通常可以击败编译器。另外,如果您可以安全地做出一些假设,但无法轻松地将它们传达给编译器,则可以使用手写汇编。)

#include <stdint.h>
#include <immintrin.h>

// compile with -masm=intel
// len must be a multiple of 32  (TODO: cleanup loop)
// buf should be 16B-aligned for best performance
size_t find_first_zero_bit_avx1(const char *bitmap, size_t len) {
    // return size_t not uint64_t.  This same code works in 32bit mode, and in the x32 ABI where pointers are 32bit

    __m128i pattern, vtmp1, vtmp2;
    const char *result_pos;
    int tmpi;

    const char *bitmap_start = bitmap;

    asm (  // modifies the bitmap pointer, but we're inside a wrapper function
      "vpcmpeqw   %[pat], %[pat],%[pat]\n\t"          // all-ones

      ".p2align 4\n\t"   // force 16B loop alignment, for the benefit of CPUs without a loop buffer
      //IACA_START  // See the godbolt link for the macro definition
      ".Lcount_loop%=:\n\t"
//      "  movdqu    %[v1], [ %[p] ]\n\t"
//      "  pcmpeqb   %[v1], %[pat]\n\t"        // for AVX: fold the load into vpcmpeqb, making sure to still use a one-register addressing mode so it can micro-fuse
//      "  movdqu    %[v2], [ %[p] + 16 ]\n\t"
//      "  pcmpeqb   %[v2], %[pat]\n\t"

      "  vpcmpeqb  %[v1], %[pat], [ %[p] ]\n\t"  // Actually use AVX, to get a big speedup over the OP's scalar code on his SnB CPU
      "  vpcmpeqb  %[v2], %[pat], [ %[p] + 16 ]\n\t"

      "  vpand     %[v2], %[v2], %[v1]\n\t"         // combine the two results from this iteration
      "  vpmovmskb  %k[result], %[v2]\n\t"
      "  cmp       %k[result], 0xFFFF\n\t"          // k modifier: eax instead of rax
      "  jne     .Lfound%=\n\t"

      "  add       %[p], 32\n\t"
      "  cmp       %[p], %[endp]\n\t"              // this is only 2 uops after the previous cmp/jcc.  We could re-arrange the loop and put the branches farther apart if needed.  (e.g. start with a vpcmpeqb outside the loop, so each iteration actually sets up for the next)
      "  jb     .Lcount_loop%=\n\t"
      //IACA_END

      // any necessary code for the not-found case, e.g. bitmap = endp
      "  mov     %[result], %[endp]\n\t"
      "  jmp    .Lend%=\n\t"

      ".Lfound%=:\n\t"                       // we have to figure out which vector the first non-match was in, based on v1 and (v2&v1)
                                  // We could just search the bytes over again, but we don't have to.
                                  // we could also check v1 first and branch, instead of checking both and using a branchless check.
      "  xor       %k[result], 0xFFFF\n\t"
      "  tzcnt     %k[result], %k[result]\n\t"  // runs as bsf on older CPUs: same result for non-zero inputs, but different flags.  Faster than bsf on AMD
      "  add       %k[result], 16\n\t"          // result = byte count in case v1 is all-ones.  In that case, v2&v1 = v2

      "  vpmovmskb %k[tmp], %[v1]\n\t"
      "  xor       %k[tmp], 0xFFFF\n\t"
      "  bsf       %k[tmp], %k[tmp]\n\t"        // bsf sets ZF if its *input* was zero.  tzcnt's flag results are based on its output.  For AMD, it would be faster to use more insns (or a branchy strategy) and avoid bsf, but Intel has fast bsf.
      "  cmovnz    %k[result], %k[tmp]\n\t"     // if there was a non-match in v1, use it instead of tzcnt(v2)+16

      "  add       %[result], %[p]\n\t"         // If we needed to force 64bit, we could use %q[p].  But size_t should be 32bit in the x32 ABI, where pointers are 32bit.  This is one advantage to using size_t over uint64_t
      ".Lend%=:\n\t"
      : [result] "=&a" (result_pos),   // force compiler to pic eax/rax to save a couple bytes of code-size from the special cmp eax, imm32  and xor eax,imm32 encodings
        [p] "+&r" (bitmap),
        // throw-away outputs to let the compiler allocate registers.  All early-clobbered so they aren't put in the same reg as an input
        [tmp] "=&r" (tmpi),
        [pat] "=&x" (pattern),
        [v1] "=&x" (vtmp1), [v2] "=&x" (vtmp2)
      : [endp] "r" (bitmap+len)
        // doesn't compile: len isn't a compile-time constant
        // , "m" ( ({ struct { char x[len]; } *dummy = (typeof(dummy))bitmap ; *dummy; }) )  // tell the compiler *which* memory is an input.
      : "memory" // we read from data pointed to by bitmap, but bitmap[0..len] isn't an input, only the pointer.
    );

    return result_pos - bitmap_start;
}

This actually compiles and assembles to asm that looks like what I expected, but I didn't test it. Note that it leaves all register allocation to the compiler, so it's more inlining-friendly. Even without inlining, it doesn't force use of a call-preserved register that has to get saved/restored (e.g. your use of a "b" constraint).

未完成:标量代码无法处理最后一个子32B数据块。

基于Agner Fog的指南/表格,对Intel SnB系列CPU进行静态性能分析。另请参阅标签wiki。我假设我们没有被缓存吞吐量限制,因此该分析仅适用于数据在L2缓存中热点或仅L1缓存足够快的情况下。

这个循环可以以每2个时钟周期一次迭代(两个向量)的速度从前端发出,因为它有7个融合域uops。(前端以4组为单位发出)。 (如果两个cmp/jcc对在同一块中解码,则实际上可能是8个uops。Haswell及更高版本可以在解码组中执行两个宏融合,但之前的CPU只能将第一个宏融合。我们可以对循环进行软件流水线处理,以便提前退出分支与p<endp分支距离更远。)

所有这些融合域uop都包括一个ALU uop,因此瓶颈将在于ALU执行端口。Haswell添加了第四个ALU单元,可以处理简单的非向量操作,包括分支,因此可以在每2个时钟(每个时钟16B)运行一次此循环。您提到的i5-2550k是SnB CPU。

我使用IACA计算每个端口的uop数量,因为手动计算很费时间。IACA很愚蠢,并认为除了循环计数器之外还存在某种迭代间依赖性,因此我必须使用-no_interiteration

g++ -masm=intel -Wall -Wextra -O3 -mtune=haswell find-first-zero-bit.cpp -c -DIACA_MARKS
iaca -64 -arch IVB -no_interiteration find-first-zero-bit.o

Intel(R) Architecture Code Analyzer Version - 2.1
Analyzed File - find-first-zero-bit.o
Binary Format - 64Bit
Architecture  - SNB
Analysis Type - Throughput

Throughput Analysis Report
--------------------------
Block Throughput: 2.50 Cycles       Throughput Bottleneck: Port1, Port5

Port Binding In Cycles Per Iteration:
-------------------------------------------------------------------------
|  Port  |  0   -  DV  |  1   |  2   -  D   |  3   -  D   |  4   |  5   |
-------------------------------------------------------------------------
| Cycles | 2.0    0.0  | 2.5  | 1.0    1.0  | 1.0    1.0  | 0.0  | 2.5  |
-------------------------------------------------------------------------

N - port number or number of cycles resource conflict caused delay, DV - Divider pipe (on port 0)
D - Data fetch pipe (on ports 2 and 3), CP - on a critical path
F - Macro Fusion with the previous instruction occurred
* - instruction micro-ops not bound to a port
^ - Micro Fusion happened
# - ESP Tracking sync uop was issued
@ - SSE instruction followed an AVX256 instruction, dozens of cycles penalty is expected
! - instruction not supported, was not accounted in Analysis

| Num Of |              Ports pressure in cycles               |    |
|  Uops  |  0  - DV  |  1  |  2  -  D  |  3  -  D  |  4  |  5  |    |
---------------------------------------------------------------------
|   2^   |           | 1.0 | 1.0   1.0 |           |     |     | CP | vpcmpeqb xmm1, xmm0, xmmword ptr [rdx]
|   2^   |           | 0.6 |           | 1.0   1.0 |     | 0.4 | CP | vpcmpeqb xmm2, xmm0, xmmword ptr [rdx+0x10]
|   1    | 0.9       | 0.1 |           |           |     | 0.1 | CP | vpand xmm2, xmm2, xmm1
|   1    | 1.0       |     |           |           |     |     |    | vpmovmskb eax, xmm2
|   1    |           |     |           |           |     | 1.0 | CP | cmp eax, 0xffff
|   0F   |           |     |           |           |     |     |    | jnz 0x18
|   1    | 0.1       | 0.9 |           |           |     |     | CP | add rdx, 0x20
|   1    |           |     |           |           |     | 1.0 | CP | cmp rdx, rsi
|   0F   |           |     |           |           |     |     |    | jb 0xffffffffffffffe1

在SnB上:pcmpeqb可以在p1/p5上运行。融合的比较和分支只能在p5上运行。非融合的cmp可以在p015上运行。无论如何,如果其中一个分支没有宏观融合,循环可以以每8/3 = 2.666个周期的速度运行一次迭代。使用宏观融合,最佳情况是7/3 = 2.333个周期。(IACA不尝试模拟uops在端口上的分配方式,就像硬件动态进行这些决策一样。然而,我们也不能期望从硬件中获得完美的调度,因此每2.5个周期内处理2个向量可能是合理的,同时发生两个宏观融合的uops有时会窃取port1或port5,从而降低吞吐量。)

正如我之前所说,Haswell更好地处理了这个循环。IACA认为HSW可以以每1.75c的速度运行循环,但显然这是错误的,因为取反的循环分支结束了问题组。它将以重复的4,3 uop模式发布。但是执行单元可以处理比此循环的前端更高的吞吐量,因此在Haswell / Broadwell / Skylake上它应该真正能够跟上前端并以每2个时钟运行一次迭代。

进一步展开更多的vpcmpeqb/vpand,每个向量只需要2个操作码(或者没有AVX时是3个,其中我们会将其加载到一个暂存区,然后将其用作pcmpeqb的目标)。因此,通过充分展开,我们应该能够每个时钟周期进行2个向量加载。如果没有AVX,使用PAND技巧是不可能做到这一点的,因为向量加载/比较/movmsk/test-and-branch是4个操作码。更大的展开使得在找到匹配的最终位置时解码更多的工作:一旦我们进入该区域,基于标量cmp的清理循环可能是一个好主意。您可以尝试使用相同的标量循环来清理非32B倍数大小的数据。
如果使用SSE,通过movdqu / pcmpeqb xmm,xmm,我们可以使用索引寻址模式而不需要额外的uops成本,因为无论寻址模式如何,movdqu加载始终只是单个加载uop。(它不需要与任何东西微融合,不像存储)。这使我们可以通过将基指针指向数组末尾以及从零开始计数的索引来节省一个循环开销的uop。例如:add %[idx], 32 / js 循环直到索引为负数。
然而,对于AVX,我们可以通过使用单寄存器寻址模式来节省2个uops,因此vpcmpeqb %[v1], %[pat], [ %[p] + 16 ]可以微融合。这意味着我们需要使用我在示例中使用的add/cmp/jcc循环结构。AVX2也适用相同的规则。

@user4602856:我有另一个减少SIMD循环开销的想法。更新了代码,应该比您的SIMD循环快1.5到2倍,在您的SnB CPU上运行。 - Peter Cordes

2
所以我认为我找到了问题所在。尽管有清单,但我认为我的内联汇编使用的寄存器与g ++使用的寄存器冲突,并破坏了测试迭代。我将g ++版本的代码作为内联汇编代码反馈,并获得了与自己相同的260000倍加速。此外,回顾一下,“加速”的计算时间太短了。

最后,我太专注于作为函数体现的代码,没有注意到g ++实际上已将函数(我正在使用-O3优化)内联到测试循环中。当我强制g ++不内联(即-fno-inline)时,260000倍的加速消失了。

我认为g ++在未经我的许可情况下内联整个函数时未考虑内联汇编代码的“清单”。

教训:我需要更好地处理内联汇编约束或使用__attribute__ ((noinline))阻止函数内联。

编辑:肯定发现g ++正在使用rax作为main()for循环计数器,与我的rax使用冲突。


GNU C内联汇编语法的一个好处是它可以内联,因此您可以将其包装在没有开销的函数中。此外,如果您的代码在内联后破坏了循环计数器,那么这可能是gcc的错误,或者更有可能是您代码中的错误。您可能会弄错约束(输出/输入/约束)。此外,最好让gcc为您选择寄存器,如果需要在asm块内部使用临时变量,则使用仅输出操作数,C代码不会触及它们。请参见此答案底部以获取指南。 - Peter Cordes
我可能会弄错限制条件,但我对所有条件都很明确。... : [count] "=a" (count) : [nBytes] "b" (nBytes), [data] "D" (data) : "r9" - codechimp
看起来还不错。你可能发现了一个gcc的bug。发布你的源代码。如果内联打破了你的代码,那么这绝对是gcc或你的代码中的一个bug。哦,你的内联汇编依赖于内存中的数据,但没有指定“memory”清除。(如果你不要求它,输入指针指向的实际数据就不被视为输入。gcc内联asm文档有一个使用结构体作为输入的示例,告诉编译器哪些数据)。你真的在编译器输出中找到了内联asm的问题吗?还是你只是猜测那是问题所在? - Peter Cordes
4
请发出内联汇编代码,这样我们才能查看约束条件、寄存器清零等。没有代码,我们只能猜测。 - David Wohlferd
我猜测问题出在哪里。我查看了但没有找到明确的寄存器冲突。但是我对这个内联汇编业务还很新,所以我只是假设我做错了什么。 - codechimp
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接