以下是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++会重复使用相同的寄存器来存储nBytes
和count
。
至于速度提升,我发现在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;
}
add
之类的操作吗?肯定有某种测量误差。请发布完整的代码。 - Peter Cordesxor r9, -1
与not r9
是相同的,两者都无法与jnz
宏融合。最好的方法是在循环外部使用mov reg,-1
,然后在循环内部使用cmp reg,[mem]
。这将使您的cmp
可以在 Intel SnB 系列 CPU 上与 jcc 宏融合,而使用立即数和内存操作数时则不可能。(请参见 http://agner.org/optimize/)。此外,您可以使用break
而不是goto done
。 - Peter Cordespcmpeqw xmm1,xmm1
获取全0xFF的向量,并使用pcmpeqb
与其进行比较。使用psubb
对比较结果求和(减去-1),偶尔使用psadbw
将字节水平求和为16位字,以避免溢出。非匹配计数=总字节数-匹配计数。或者,要在找到第一个匹配项时中断,请在比较结果上使用pmovmskb eax,xmm0
,并使用cmp eax,0xFFFF / jne
仅在所有16个向量元素都相等时才跳过。 - Peter Cordesbsf
只是我的疏忽,但在这一点上已经无关紧要,因为:1)我的CPU实际上不支持真正的tzcnt
,2)bsf
仅在最后发生一次。我的CPU是i5-2550K。 - codechimp