用 64 位替换 32 位循环计数器会在 Intel CPU 上使用 _mm_popcnt_u64 引入疯狂的性能偏差
Posted
技术标签:
【中文标题】用 64 位替换 32 位循环计数器会在 Intel CPU 上使用 _mm_popcnt_u64 引入疯狂的性能偏差【英文标题】:Replacing a 32-bit loop counter with 64-bit introduces crazy performance deviations with _mm_popcnt_u64 on Intel CPUs 【发布时间】:2014-09-24 13:01:01 【问题描述】:我一直在寻找popcount
大型数据数组的最快方法。我遇到了一个非常奇怪的效果:将循环变量从 unsigned
更改为 uint64_t
导致我的 PC 上的性能下降了 50%。
基准
#include <iostream>
#include <chrono>
#include <x86intrin.h>
int main(int argc, char* argv[])
using namespace std;
if (argc != 2)
cerr << "usage: array_size in MB" << endl;
return -1;
uint64_t size = atol(argv[1])<<20;
uint64_t* buffer = new uint64_t[size/8];
char* charbuffer = reinterpret_cast<char*>(buffer);
for (unsigned i=0; i<size; ++i)
charbuffer[i] = rand()%256;
uint64_t count,duration;
chrono::time_point<chrono::system_clock> startP,endP;
startP = chrono::system_clock::now();
count = 0;
for( unsigned k = 0; k < 10000; k++)
// Tight unrolled loop with unsigned
for (unsigned i=0; i<size/8; i+=4)
count += _mm_popcnt_u64(buffer[i]);
count += _mm_popcnt_u64(buffer[i+1]);
count += _mm_popcnt_u64(buffer[i+2]);
count += _mm_popcnt_u64(buffer[i+3]);
endP = chrono::system_clock::now();
duration = chrono::duration_cast<std::chrono::nanoseconds>(endP-startP).count();
cout << "unsigned\t" << count << '\t' << (duration/1.0E9) << " sec \t"
<< (10000.0*size)/(duration) << " GB/s" << endl;
startP = chrono::system_clock::now();
count=0;
for( unsigned k = 0; k < 10000; k++)
// Tight unrolled loop with uint64_t
for (uint64_t i=0;i<size/8;i+=4)
count += _mm_popcnt_u64(buffer[i]);
count += _mm_popcnt_u64(buffer[i+1]);
count += _mm_popcnt_u64(buffer[i+2]);
count += _mm_popcnt_u64(buffer[i+3]);
endP = chrono::system_clock::now();
duration = chrono::duration_cast<std::chrono::nanoseconds>(endP-startP).count();
cout << "uint64_t\t" << count << '\t' << (duration/1.0E9) << " sec \t"
<< (10000.0*size)/(duration) << " GB/s" << endl;
free(charbuffer);
如您所见,我们创建了一个随机数据缓冲区,大小为x
兆字节,其中x
是从命令行读取的。之后,我们遍历缓冲区并使用 x86 popcount
内部函数的展开版本来执行 popcount。为了获得更精确的结果,我们执行 popcount 10,000 次。我们测量 popcount 的时间。在大写中,内循环变量为unsigned
,在小写中,内循环变量为uint64_t
。我以为这应该没什么区别,但事实恰恰相反。
(绝对疯狂的)结果
我是这样编译的(g++版本:Ubuntu 4.8.2-19ubuntu1):
g++ -O3 -march=native -std=c++11 test.cpp -o test
这是我的 Haswell Core i7-4770K CPU @ 3.50 GHz 上运行 test 1
的结果(所以 1 MB 随机数据):
如您所见,uint64_t
版本的吞吐量只有unsigned
版本的一半!问题似乎是生成了不同的程序集,但为什么呢?首先想到了一个编译器的bug,于是尝试了clang++
(Ubuntu Clang version 3.4-1ubuntu3):
clang++ -O3 -march=native -std=c++11 teest.cpp -o test
结果:test 1
所以,结果几乎一样,还是很奇怪。 但现在它变得超级奇怪。我用常量1
替换了从输入中读取的缓冲区大小,所以我改变了:
uint64_t size = atol(argv[1]) << 20;
到
uint64_t size = 1 << 20;
因此,编译器现在在编译时就知道缓冲区大小。也许它可以添加一些优化!以下是g++
的号码:
现在,两个版本的速度都一样快。然而,unsigned
变得更慢了!它从26
下降到20 GB/s
,因此用常数值替换非常数会导致去优化。说真的,我不知道这里发生了什么!但是现在用新版本来clang++
:
等等,什么?现在,两个版本都降到了 慢 15 GB/s 的速度。因此,用常数值替换非常数甚至会导致 Clang 的 两种情况 的代码变慢!
我请一位使用Ivy Bridge CPU 的同事编译我的基准测试。他得到了类似的结果,所以似乎不是Haswell。因为两个编译器在这里产生了奇怪的结果,所以它似乎也不是编译器的错误。我们这里没有 AMD CPU,所以只能用 Intel 进行测试。
更多的疯狂,拜托!
以第一个示例(带有atol(argv[1])
的示例)并在变量前加上static
,即:
static uint64_t size=atol(argv[1])<<20;
这是我在 g++ 中的结果:
无符号 41959360000 0.396728 秒 26.4306 GB/s uint64_t 41959360000 0.509484 秒 20.5811 GB/s是的,还有另一种选择。我们仍然拥有u32
的快速 26 GB/s,但我们设法将 u64
至少从 13 GB/s 版本提升到了 20 GB/s 版本! 在我同事的 PC 上,u64
版本比 u32
版本更快,产生了最快的结果。 遗憾的是,这仅适用于 g++
,clang++
似乎不适用关心static
。
我的问题
你能解释一下这些结果吗?特别是:
u32
和u64
怎么会有这么大的区别?
如何用恒定缓冲区大小替换非常数触发不太理想的代码?
static
关键字的插入如何使u64
循环更快?甚至比我同事电脑上的原始代码还要快!
我知道优化是一个棘手的领域,但是,我从没想过这么小的变化会导致执行时间100% 的差异,并且像恒定缓冲区大小这样的小因素会再次混合结果完全。当然,我一直希望拥有能够popcount 26 GB/s 的版本。我能想到的唯一可靠的方法是复制粘贴这种情况下的程序集并使用内联程序集。这是我可以摆脱那些似乎对小改动发疯的编译器的唯一方法。你怎么看?还有其他方法可以可靠地获得性能最高的代码吗?
反汇编
这里是各种结果的反汇编:
来自 g++ / u32 / non-const bufsize 的 26 GB/s 版本:
0x400af8:
lea 0x1(%rdx),%eax
popcnt (%rbx,%rax,8),%r9
lea 0x2(%rdx),%edi
popcnt (%rbx,%rcx,8),%rax
lea 0x3(%rdx),%esi
add %r9,%rax
popcnt (%rbx,%rdi,8),%rcx
add $0x4,%edx
add %rcx,%rax
popcnt (%rbx,%rsi,8),%rcx
add %rcx,%rax
mov %edx,%ecx
add %rax,%r14
cmp %rbp,%rcx
jb 0x400af8
13 GB/s 版本来自 g++ / u64 / non-const bufsize:
0x400c00:
popcnt 0x8(%rbx,%rdx,8),%rcx
popcnt (%rbx,%rdx,8),%rax
add %rcx,%rax
popcnt 0x10(%rbx,%rdx,8),%rcx
add %rcx,%rax
popcnt 0x18(%rbx,%rdx,8),%rcx
add $0x4,%rdx
add %rcx,%rax
add %rax,%r12
cmp %rbp,%rdx
jb 0x400c00
来自 clang++ / u64 / non-const bufsize 的 15 GB/s 版本:
0x400e50:
popcnt (%r15,%rcx,8),%rdx
add %rbx,%rdx
popcnt 0x8(%r15,%rcx,8),%rsi
add %rdx,%rsi
popcnt 0x10(%r15,%rcx,8),%rdx
add %rsi,%rdx
popcnt 0x18(%r15,%rcx,8),%rbx
add %rdx,%rbx
add $0x4,%rcx
cmp %rbp,%rcx
jb 0x400e50
来自 g++ / u32&u64 / const bufsize 的 20 GB/s 版本:
0x400a68:
popcnt (%rbx,%rdx,1),%rax
popcnt 0x8(%rbx,%rdx,1),%rcx
add %rax,%rcx
popcnt 0x10(%rbx,%rdx,1),%rax
add %rax,%rcx
popcnt 0x18(%rbx,%rdx,1),%rsi
add $0x20,%rdx
add %rsi,%rcx
add %rcx,%rbp
cmp $0x100000,%rdx
jne 0x400a68
来自 clang++ / u32&u64 / const bufsize 的 15 GB/s 版本:
0x400dd0:
popcnt (%r14,%rcx,8),%rdx
add %rbx,%rdx
popcnt 0x8(%r14,%rcx,8),%rsi
add %rdx,%rsi
popcnt 0x10(%r14,%rcx,8),%rdx
add %rsi,%rdx
popcnt 0x18(%r14,%rcx,8),%rbx
add %rdx,%rbx
add $0x4,%rcx
cmp $0x20000,%rcx
jb 0x400dd0
有趣的是,最快 (26 GB/s) 的版本也是最长的!它似乎是唯一使用lea
的解决方案。有些版本使用jb
跳转,有些版本使用jne
。但除此之外,所有版本似乎都具有可比性。我看不出 100% 的性能差距可能来自哪里,但我不太擅长破译汇编。最慢的 (13 GB/s) 版本看起来甚至非常短而且很好。谁能解释一下?
经验教训
无论这个问题的答案是什么;我了解到,在真正的热循环中,每个细节都很重要,甚至是似乎与热代码没有任何关联的细节。我从来没有想过要为循环变量使用什么类型,但正如您所见,如此微小的变化可以产生 100% 的不同!即使是缓冲区的存储类型也会产生巨大的差异,正如我们在 size 变量前面插入 static
关键字所看到的!将来,在编写对系统性能至关重要的非常紧且热的循环时,我将始终在各种编译器上测试各种替代方案。
有趣的是,尽管我已经展开了四次循环,但性能差异仍然如此之大。因此,即使展开,您仍然会受到主要性能偏差的影响。很有趣。
【问题讨论】:
这么多评论!你可以view them in chat,如果你愿意,甚至可以留下你自己的,但请不要在这里添加任何东西! 另见GCC Issue 62011, False Data Dependency in popcnt instruction。其他人提供了它,但它似乎在清理过程中丢失了。 我不知道,但它是静态版本的反汇编之一?如果没有,您可以编辑帖子并添加它吗? 【参考方案1】:这不是答案,而是 2021 年少数编译器的反馈。 在英特尔 CoffeeLake 9900k 上。
使用 Microsoft 编译器 (VS2019),工具集 v142:
unsigned 209695540000 1.8322 sec 28.6152 GB/s uint64_t 209695540000 3.08764 sec 16.9802 GB/s
使用英特尔编译器 2021:
unsigned 209695540000 1.70845 sec 30.688 GB/s uint64_t 209695540000 1.57956 sec 33.1921 GB/s
根据 Mysticial 的回答,Intel 编译器知道 False Data Dependency,但不知道 Microsoft 编译器。
对于 intel 编译器,我使用了/QxHost
(优化了主机的 CPU 架构)/Oi
(启用内部函数)和#include <nmmintrin.h>
,而不是#include <immintrin.h>
。
完整编译命令:/GS /W3 /QxHost /Gy /Zi /O2 /D "NDEBUG" /D "_CONSOLE" /D "_UNICODE" /D "UNICODE" /Qipo /Zc:forScope /Oi /MD /Fa"x64\Release\" /EHsc /nologo /Fo"x64\Release\" //fprofile-instr-use "x64\Release\" /Fp"x64\Release\Benchmark.pch"
.
来自 ICC 的反编译(IDA 7.5)程序集:
int __cdecl main(int argc, const char **argv, const char **envp)
int v6; // er13
_BYTE *v8; // rsi
unsigned int v9; // edi
unsigned __int64 i; // rbx
unsigned __int64 v11; // rdi
int v12; // ebp
__int64 v13; // r14
__int64 v14; // rbx
unsigned int v15; // eax
unsigned __int64 v16; // rcx
unsigned int v17; // eax
unsigned __int64 v18; // rcx
__int64 v19; // rdx
unsigned int v20; // eax
int result; // eax
std::ostream *v23; // rbx
char v24; // dl
std::ostream *v33; // rbx
std::ostream *v41; // rbx
__int64 v42; // rdx
unsigned int v43; // eax
int v44; // ebp
__int64 v45; // r14
__int64 v46; // rbx
unsigned __int64 v47; // rax
unsigned __int64 v48; // rax
std::ostream *v50; // rdi
char v51; // dl
std::ostream *v58; // rdi
std::ostream *v60; // rdi
__int64 v61; // rdx
unsigned int v62; // eax
__asm
vmovdqa [rsp+98h+var_58], xmm8
vmovapd [rsp+98h+var_68], xmm7
vmovapd [rsp+98h+var_78], xmm6
if ( argc == 2 )
v6 = atol(argv[1]) << 20;
_R15 = v6;
v8 = operator new[](v6);
if ( v6 )
v9 = 1;
for ( i = 0i64; i < v6; i = v9++ )
v8[i] = rand();
v11 = (unsigned __int64)v6 >> 3;
v12 = 0;
v13 = Xtime_get_ticks_0();
v14 = 0i64;
do
if ( v6 )
v15 = 4;
v16 = 0i64;
do
v14 += __popcnt(*(_QWORD *)&v8[8 * v16])
+ __popcnt(*(_QWORD *)&v8[8 * v15 - 24])
+ __popcnt(*(_QWORD *)&v8[8 * v15 - 16])
+ __popcnt(*(_QWORD *)&v8[8 * v15 - 8]);
v16 = v15;
v15 += 4;
while ( v11 > v16 );
v17 = 4;
v18 = 0i64;
do
v14 += __popcnt(*(_QWORD *)&v8[8 * v18])
+ __popcnt(*(_QWORD *)&v8[8 * v17 - 24])
+ __popcnt(*(_QWORD *)&v8[8 * v17 - 16])
+ __popcnt(*(_QWORD *)&v8[8 * v17 - 8]);
v18 = v17;
v17 += 4;
while ( v11 > v18 );
v12 += 2;
while ( v12 != 10000 );
_RBP = 100 * (Xtime_get_ticks_0() - v13);
std::operator___std::char_traits_char___(std::cout, "unsigned\t");
v23 = (std::ostream *)std::ostream::operator<<(std::cout, v14);
std::operator___std::char_traits_char____0(v23, v24);
__asm
vmovq xmm0, rbp
vmovdqa xmm8, cs:__xmm@00000000000000004530000043300000
vpunpckldq xmm0, xmm0, xmm8
vmovapd xmm7, cs:__xmm@45300000000000004330000000000000
vsubpd xmm0, xmm0, xmm7
vpermilpd xmm1, xmm0, 1
vaddsd xmm6, xmm1, xmm0
vdivsd xmm1, xmm6, cs:__real@41cdcd6500000000
v33 = (std::ostream *)std::ostream::operator<<(v23);
std::operator___std::char_traits_char___(v33, " sec \t");
__asm
vmovq xmm0, r15
vpunpckldq xmm0, xmm0, xmm8
vsubpd xmm0, xmm0, xmm7
vpermilpd xmm1, xmm0, 1
vaddsd xmm0, xmm1, xmm0
vmulsd xmm7, xmm0, cs:__real@40c3880000000000
vdivsd xmm1, xmm7, xmm6
v41 = (std::ostream *)std::ostream::operator<<(v33);
std::operator___std::char_traits_char___(v41, " GB/s");
LOBYTE(v42) = 10;
v43 = std::ios::widen((char *)v41 + *(int *)(*(_QWORD *)v41 + 4i64), v42);
std::ostream::put(v41, v43);
std::ostream::flush(v41);
v44 = 0;
v45 = Xtime_get_ticks_0();
v46 = 0i64;
do
if ( v6 )
v47 = 0i64;
do
v46 += __popcnt(*(_QWORD *)&v8[8 * v47])
+ __popcnt(*(_QWORD *)&v8[8 * v47 + 8])
+ __popcnt(*(_QWORD *)&v8[8 * v47 + 16])
+ __popcnt(*(_QWORD *)&v8[8 * v47 + 24]);
v47 += 4i64;
while ( v47 < v11 );
v48 = 0i64;
do
v46 += __popcnt(*(_QWORD *)&v8[8 * v48])
+ __popcnt(*(_QWORD *)&v8[8 * v48 + 8])
+ __popcnt(*(_QWORD *)&v8[8 * v48 + 16])
+ __popcnt(*(_QWORD *)&v8[8 * v48 + 24]);
v48 += 4i64;
while ( v48 < v11 );
v44 += 2;
while ( v44 != 10000 );
_RBP = 100 * (Xtime_get_ticks_0() - v45);
std::operator___std::char_traits_char___(std::cout, "uint64_t\t");
v50 = (std::ostream *)std::ostream::operator<<(std::cout, v46);
std::operator___std::char_traits_char____0(v50, v51);
__asm
vmovq xmm0, rbp
vpunpckldq xmm0, xmm0, cs:__xmm@00000000000000004530000043300000
vsubpd xmm0, xmm0, cs:__xmm@45300000000000004330000000000000
vpermilpd xmm1, xmm0, 1
vaddsd xmm6, xmm1, xmm0
vdivsd xmm1, xmm6, cs:__real@41cdcd6500000000
v58 = (std::ostream *)std::ostream::operator<<(v50);
std::operator___std::char_traits_char___(v58, " sec \t");
__asm vdivsd xmm1, xmm7, xmm6
v60 = (std::ostream *)std::ostream::operator<<(v58);
std::operator___std::char_traits_char___(v60, " GB/s");
LOBYTE(v61) = 10;
v62 = std::ios::widen((char *)v60 + *(int *)(*(_QWORD *)v60 + 4i64), v61);
std::ostream::put(v60, v62);
std::ostream::flush(v60);
free(v8);
result = 0;
else
std::operator___std::char_traits_char___(std::cerr, "usage: array_size in MB");
LOBYTE(v19) = 10;
v20 = std::ios::widen((char *)&std::cerr + *((int *)std::cerr + 1), v19);
std::ostream::put(std::cerr, v20);
std::ostream::flush(std::cerr);
result = -1;
__asm
vmovaps xmm6, [rsp+98h+var_78]
vmovaps xmm7, [rsp+98h+var_68]
vmovaps xmm8, [rsp+98h+var_58]
return result;
和main的反汇编:
.text:0140001000 .686p
.text:0140001000 .mmx
.text:0140001000 .model flat
.text:0140001000
.text:0140001000 ; ===========================================================================
.text:0140001000
.text:0140001000 ; Segment type: Pure code
.text:0140001000 ; Segment permissions: Read/Execute
.text:0140001000 _text segment para public 'CODE' use64
.text:0140001000 assume cs:_text
.text:0140001000 ;org 140001000h
.text:0140001000 assume es:nothing, ss:nothing, ds:_data, fs:nothing, gs:nothing
.text:0140001000
.text:0140001000 ; =============== S U B R O U T I N E =======================================
.text:0140001000
.text:0140001000
.text:0140001000 ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:0140001000 main proc near ; CODE XREF: __scrt_common_main_seh+107↓p
.text:0140001000 ; DATA XREF: .pdata:ExceptionDir↓o
.text:0140001000
.text:0140001000 var_78 = xmmword ptr -78h
.text:0140001000 var_68 = xmmword ptr -68h
.text:0140001000 var_58 = xmmword ptr -58h
.text:0140001000
.text:0140001000 push r15
.text:0140001002 push r14
.text:0140001004 push r13
.text:0140001006 push r12
.text:0140001008 push rsi
.text:0140001009 push rdi
.text:014000100A push rbp
.text:014000100B push rbx
.text:014000100C sub rsp, 58h
.text:0140001010 vmovdqa [rsp+98h+var_58], xmm8
.text:0140001016 vmovapd [rsp+98h+var_68], xmm7
.text:014000101C vmovapd [rsp+98h+var_78], xmm6
.text:0140001022 cmp ecx, 2
.text:0140001025 jnz loc_14000113E
.text:014000102B mov rcx, [rdx+8] ; String
.text:014000102F call cs:__imp_atol
.text:0140001035 mov r13d, eax
.text:0140001038 shl r13d, 14h
.text:014000103C movsxd r15, r13d
.text:014000103F mov rcx, r15 ; size
.text:0140001042 call ??_U@YAPEAX_K@Z ; operator new[](unsigned __int64)
.text:0140001047 mov rsi, rax
.text:014000104A test r15d, r15d
.text:014000104D jz short loc_14000106E
.text:014000104F mov edi, 1
.text:0140001054 xor ebx, ebx
.text:0140001056 mov rbp, cs:__imp_rand
.text:014000105D nop dword ptr [rax]
.text:0140001060
.text:0140001060 loc_140001060: ; CODE XREF: main+6C↓j
.text:0140001060 call rbp ; __imp_rand
.text:0140001062 mov [rsi+rbx], al
.text:0140001065 mov ebx, edi
.text:0140001067 inc edi
.text:0140001069 cmp rbx, r15
.text:014000106C jb short loc_140001060
.text:014000106E
.text:014000106E loc_14000106E: ; CODE XREF: main+4D↑j
.text:014000106E mov rdi, r15
.text:0140001071 shr rdi, 3
.text:0140001075 xor ebp, ebp
.text:0140001077 call _Xtime_get_ticks_0
.text:014000107C mov r14, rax
.text:014000107F xor ebx, ebx
.text:0140001081 jmp short loc_14000109F
.text:0140001081 ; ---------------------------------------------------------------------------
.text:0140001083 align 10h
.text:0140001090
.text:0140001090 loc_140001090: ; CODE XREF: main+A2↓j
.text:0140001090 ; main+EC↓j ...
.text:0140001090 add ebp, 2
.text:0140001093 cmp ebp, 2710h
.text:0140001099 jz loc_140001184
.text:014000109F
.text:014000109F loc_14000109F: ; CODE XREF: main+81↑j
.text:014000109F test r13d, r13d
.text:01400010A2 jz short loc_140001090
.text:01400010A4 mov eax, 4
.text:01400010A9 xor ecx, ecx
.text:01400010AB nop dword ptr [rax+rax+00h]
.text:01400010B0
.text:01400010B0 loc_1400010B0: ; CODE XREF: main+E7↓j
.text:01400010B0 popcnt rcx, qword ptr [rsi+rcx*8]
.text:01400010B6 add rcx, rbx
.text:01400010B9 lea edx, [rax-3]
.text:01400010BC popcnt rdx, qword ptr [rsi+rdx*8]
.text:01400010C2 add rdx, rcx
.text:01400010C5 lea ecx, [rax-2]
.text:01400010C8 popcnt rcx, qword ptr [rsi+rcx*8]
.text:01400010CE add rcx, rdx
.text:01400010D1 lea edx, [rax-1]
.text:01400010D4 xor ebx, ebx
.text:01400010D6 popcnt rbx, qword ptr [rsi+rdx*8]
.text:01400010DC add rbx, rcx
.text:01400010DF mov ecx, eax
.text:01400010E1 add eax, 4
.text:01400010E4 cmp rdi, rcx
.text:01400010E7 ja short loc_1400010B0
.text:01400010E9 test r13d, r13d
.text:01400010EC jz short loc_140001090
.text:01400010EE mov eax, 4
.text:01400010F3 xor ecx, ecx
.text:01400010F5 db 2Eh
.text:01400010F5 nop word ptr [rax+rax+00000000h]
.text:01400010FF nop
.text:0140001100
.text:0140001100 loc_140001100: ; CODE XREF: main+137↓j
.text:0140001100 popcnt rcx, qword ptr [rsi+rcx*8]
.text:0140001106 add rcx, rbx
.text:0140001109 lea edx, [rax-3]
.text:014000110C popcnt rdx, qword ptr [rsi+rdx*8]
.text:0140001112 add rdx, rcx
.text:0140001115 lea ecx, [rax-2]
.text:0140001118 popcnt rcx, qword ptr [rsi+rcx*8]
.text:014000111E add rcx, rdx
.text:0140001121 lea edx, [rax-1]
.text:0140001124 xor ebx, ebx
.text:0140001126 popcnt rbx, qword ptr [rsi+rdx*8]
.text:014000112C add rbx, rcx
.text:014000112F mov ecx, eax
.text:0140001131 add eax, 4
.text:0140001134 cmp rdi, rcx
.text:0140001137 ja short loc_140001100
.text:0140001139 jmp loc_140001090
.text:014000113E ; ---------------------------------------------------------------------------
.text:014000113E
.text:014000113E loc_14000113E: ; CODE XREF: main+25↑j
.text:014000113E mov rsi, cs:__imp_?cerr@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A ; std::ostream std::cerr
.text:0140001145 lea rdx, aUsageArraySize ; "usage: array_size in MB"
.text:014000114C mov rcx, rsi ; std::ostream *
.text:014000114F call std__operator___std__char_traits_char___
.text:0140001154 mov rax, [rsi]
.text:0140001157 movsxd rcx, dword ptr [rax+4]
.text:014000115B add rcx, rsi
.text:014000115E mov dl, 0Ah
.text:0140001160 call cs:__imp_?widen@?$basic_ios@DU?$char_traits@D@std@@@std@@QEBADD@Z ; std::ios::widen(char)
.text:0140001166 mov rcx, rsi
.text:0140001169 mov edx, eax
.text:014000116B call cs:__imp_?put@?$basic_ostream@DU?$char_traits@D@std@@@std@@QEAAAEAV12@D@Z ; std::ostream::put(char)
.text:0140001171 mov rcx, rsi
.text:0140001174 call cs:__imp_?flush@?$basic_ostream@DU?$char_traits@D@std@@@std@@QEAAAEAV12@XZ ; std::ostream::flush(void)
.text:014000117A mov eax, 0FFFFFFFFh
.text:014000117F jmp loc_1400013E2
.text:0140001184 ; ---------------------------------------------------------------------------
.text:0140001184
.text:0140001184 loc_140001184: ; CODE XREF: main+99↑j
.text:0140001184 call _Xtime_get_ticks_0
.text:0140001189 sub rax, r14
.text:014000118C imul rbp, rax, 64h ; 'd'
.text:0140001190 mov r14, cs:__imp_?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A ; std::ostream std::cout
.text:0140001197 lea rdx, aUnsigned ; "unsigned\t"
.text:014000119E mov rcx, r14 ; std::ostream *
.text:01400011A1 call std__operator___std__char_traits_char___
.text:01400011A6 mov rcx, r14
.text:01400011A9 mov rdx, rbx
.text:01400011AC call cs:__imp_??6?$basic_ostream@DU?$char_traits@D@std@@@std@@QEAAAEAV01@_K@Z ; std::ostream::operator<<(unsigned __int64)
.text:01400011B2 mov rbx, rax
.text:01400011B5 mov rcx, rax ; std::ostream *
.text:01400011B8 call std__operator___std__char_traits_char____0
.text:01400011BD vmovq xmm0, rbp
.text:01400011C2 vmovdqa xmm8, cs:__xmm@00000000000000004530000043300000
.text:01400011CA vpunpckldq xmm0, xmm0, xmm8
.text:01400011CF vmovapd xmm7, cs:__xmm@45300000000000004330000000000000
.text:01400011D7 vsubpd xmm0, xmm0, xmm7
.text:01400011DB vpermilpd xmm1, xmm0, 1
.text:01400011E1 vaddsd xmm6, xmm1, xmm0
.text:01400011E5 vdivsd xmm1, xmm6, cs:__real@41cdcd6500000000
.text:01400011ED mov r12, cs:__imp_??6?$basic_ostream@DU?$char_traits@D@std@@@std@@QEAAAEAV01@N@Z ; std::ostream::operator<<(double)
.text:01400011F4 mov rcx, rbx
.text:01400011F7 call r12 ; std::ostream::operator<<(double) ; std::ostream::operator<<(double)
.text:01400011FA mov rbx, rax
.text:01400011FD lea rdx, aSec ; " sec \t"
.text:0140001204 mov rcx, rax ; std::ostream *
.text:0140001207 call std__operator___std__char_traits_char___
.text:014000120C vmovq xmm0, r15
.text:0140001211 vpunpckldq xmm0, xmm0, xmm8
.text:0140001216 vsubpd xmm0, xmm0, xmm7
.text:014000121A vpermilpd xmm1, xmm0, 1
.text:0140001220 vaddsd xmm0, xmm1, xmm0
.text:0140001224 vmulsd xmm7, xmm0, cs:__real@40c3880000000000
.text:014000122C vdivsd xmm1, xmm7, xmm6
.text:0140001230 mov rcx, rbx
.text:0140001233 call r12 ; std::ostream::operator<<(double) ; std::ostream::operator<<(double)
.text:0140001236 mov rbx, rax
.text:0140001239 lea rdx, aGbS ; " GB/s"
.text:0140001240 mov rcx, rax ; std::ostream *
.text:0140001243 call std__operator___std__char_traits_char___
.text:0140001248 mov rax, [rbx]
.text:014000124B movsxd rcx, dword ptr [rax+4]
.text:014000124F add rcx, rbx
.text:0140001252 mov dl, 0Ah
.text:0140001254 call cs:__imp_?widen@?$basic_ios@DU?$char_traits@D@std@@@std@@QEBADD@Z ; std::ios::widen(char)
.text:014000125A mov rcx, rbx
.text:014000125D mov edx, eax
.text:014000125F call cs:__imp_?put@?$basic_ostream@DU?$char_traits@D@std@@@std@@QEAAAEAV12@D@Z ; std::ostream::put(char)
.text:0140001265 mov rcx, rbx
.text:0140001268 call cs:__imp_?flush@?$basic_ostream@DU?$char_traits@D@std@@@std@@QEAAAEAV12@XZ ; std::ostream::flush(void)
.text:014000126E xor ebp, ebp
.text:0140001270 call _Xtime_get_ticks_0
.text:0140001275 mov r14, rax
.text:0140001278 xor ebx, ebx
.text:014000127A jmp short loc_14000128F
.text:014000127A ; ---------------------------------------------------------------------------
.text:014000127C align 20h
.text:0140001280
.text:0140001280 loc_140001280: ; CODE XREF: main+292↓j
.text:0140001280 ; main+2DB↓j ...
.text:0140001280 add ebp, 2
.text:0140001283 cmp ebp, 2710h
.text:0140001289 jz loc_14000131D
.text:014000128F
.text:014000128F loc_14000128F: ; CODE XREF: main+27A↑j
.text:014000128F test r13d, r13d
.text:0140001292 jz short loc_140001280
.text:0140001294 xor eax, eax
.text:0140001296 db 2Eh
.text:0140001296 nop word ptr [rax+rax+00000000h]
.text:01400012A0
.text:01400012A0 loc_1400012A0: ; CODE XREF: main+2D6↓j
.text:01400012A0 xor ecx, ecx
.text:01400012A2 popcnt rcx, qword ptr [rsi+rax*8]
.text:01400012A8 add rcx, rbx
.text:01400012AB xor edx, edx
.text:01400012AD popcnt rdx, qword ptr [rsi+rax*8+8]
.text:01400012B4 add rdx, rcx
.text:01400012B7 xor ecx, ecx
.text:01400012B9 popcnt rcx, qword ptr [rsi+rax*8+10h]
.text:01400012C0 add rcx, rdx
.text:01400012C3 xor ebx, ebx
.text:01400012C5 popcnt rbx, qword ptr [rsi+rax*8+18h]
.text:01400012CC add rbx, rcx
.text:01400012CF add rax, 4
.text:01400012D3 cmp rax, rdi
.text:01400012D6 jb short loc_1400012A0
.text:01400012D8 test r13d, r13d
.text:01400012DB jz short loc_140001280
.text:01400012DD xor eax, eax
.text:01400012DF nop
.text:01400012E0
.text:01400012E0 loc_1400012E0: ; CODE XREF: main+316↓j
.text:01400012E0 xor ecx, ecx
.text:01400012E2 popcnt rcx, qword ptr [rsi+rax*8]
.text:01400012E8 add rcx, rbx
.text:01400012EB xor edx, edx
.text:01400012ED popcnt rdx, qword ptr [rsi+rax*8+8]
.text:01400012F4 add rdx, rcx
.text:01400012F7 xor ecx, ecx
.text:01400012F9 popcnt rcx, qword ptr [rsi+rax*8+10h]
.text:0140001300 add rcx, rdx
.text:0140001303 xor ebx, ebx
.text:0140001305 popcnt rbx, qword ptr [rsi+rax*8+18h]
.text:014000130C add rbx, rcx
.text:014000130F add rax, 4
.text:0140001313 cmp rax, rdi
.text:0140001316 jb short loc_1400012E0
.text:0140001318 jmp loc_140001280
.text:014000131D ; ---------------------------------------------------------------------------
.text:014000131D
.text:014000131D loc_14000131D: ; CODE XREF: main+289↑j
.text:014000131D call _Xtime_get_ticks_0
.text:0140001322 sub rax, r14
.text:0140001325 imul rbp, rax, 64h ; 'd'
.text:0140001329 mov rdi, cs:__imp_?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A ; std::ostream std::cout
.text:0140001330 lea rdx, aUint64T ; "uint64_t\t"
.text:0140001337 mov rcx, rdi ; std::ostream *
.text:014000133A call std__operator___std__char_traits_char___
.text:014000133F mov rcx, rdi
.text:0140001342 mov rdx, rbx
.text:0140001345 call cs:__imp_??6?$basic_ostream@DU?$char_traits@D@std@@@std@@QEAAAEAV01@_K@Z ; std::ostream::operator<<(unsigned __int64)
.text:014000134B mov rdi, rax
.text:014000134E mov rcx, rax ; std::ostream *
.text:0140001351 call std__operator___std__char_traits_char____0
.text:0140001356 vmovq xmm0, rbp
.text:014000135B vpunpckldq xmm0, xmm0, cs:__xmm@00000000000000004530000043300000
.text:0140001363 vsubpd xmm0, xmm0, cs:__xmm@45300000000000004330000000000000
.text:014000136B vpermilpd xmm1, xmm0, 1
.text:0140001371 vaddsd xmm6, xmm1, xmm0
.text:0140001375 vdivsd xmm1, xmm6, cs:__real@41cdcd6500000000
.text:014000137D mov rcx, rdi
.text:0140001380 call r12 ; std::ostream::operator<<(double) ; std::ostream::operator<<(double)
.text:0140001383 mov rdi, rax
.text:0140001386 lea rdx, aSec ; " sec \t"
.text:014000138D mov rcx, rax ; std::ostream *
.text:0140001390 call std__operator___std__char_traits_char___
.text:0140001395 vdivsd xmm1, xmm7, xmm6
.text:0140001399 mov rcx, rdi
.text:014000139C call r12 ; std::ostream::operator<<(double) ; std::ostream::operator<<(double)
.text:014000139F mov rdi, rax
.text:01400013A2 lea rdx, aGbS ; " GB/s"
.text:01400013A9 mov rcx, rax ; std::ostream *
.text:01400013AC call std__operator___std__char_traits_char___
.text:01400013B1 mov rax, [rdi]
.text:01400013B4 movsxd rcx, dword ptr [rax+4]
.text:01400013B8 add rcx, rdi
.text:01400013BB mov dl, 0Ah
.text:01400013BD call cs:__imp_?widen@?$basic_ios@DU?$char_traits@D@std@@@std@@QEBADD@Z ; std::ios::widen(char)
.text:01400013C3 mov rcx, rdi
.text:01400013C6 mov edx, eax
.text:01400013C8 call cs:__imp_?put@?$basic_ostream@DU?$char_traits@D@std@@@std@@QEAAAEAV12@D@Z ; std::ostream::put(char)
.text:01400013CE mov rcx, rdi
.text:01400013D1 call cs:__imp_?flush@?$basic_ostream@DU?$char_traits@D@std@@@std@@QEAAAEAV12@XZ ; std::ostream::flush(void)
.text:01400013D7 mov rcx, rsi ; Block
.text:01400013DA call cs:__imp_free
.text:01400013E0 xor eax, eax
.text:01400013E2
.text:01400013E2 loc_1400013E2: ; CODE XREF: main+17F↑j
.text:01400013E2 vmovaps xmm6, [rsp+98h+var_78]
.text:01400013E8 vmovaps xmm7, [rsp+98h+var_68]
.text:01400013EE vmovaps xmm8, [rsp+98h+var_58]
.text:01400013F4 add rsp, 58h
.text:01400013F8 pop rbx
.text:01400013F9 pop rbp
.text:01400013FA pop rdi
.text:01400013FB pop rsi
.text:01400013FC pop r12
.text:01400013FE pop r13
.text:0140001400 pop r14
.text:0140001402 pop r15
.text:0140001404 retn
.text:0140001404 main endp
Coffee lake specification update "POPCNT 指令的执行时间可能比预期的要长"。
【讨论】:
你是如何用 ICC 编译的? godbolt.org/z/aWxr95 显示 ICC-O3 -march=skylake
反转 k = 0 .. 10000 重复循环,将 4 个 popcnt 结果相加,然后出于某种疯狂的原因广播到 YMM 寄存器并将 10k 次(而不是乘以一次)添加到向量累加器 (ymm2)然后水平求和。这应该会产生人为地高于每个时钟周期一个 8 字节 popcnt 的结果。 (我认为;除非 SIMD 循环实际上没有并行执行 4 件有用的事情。)
无论如何,ICC 小心翼翼地执行popcnt same,same
以避免错误的 dep,但看起来它正在击败这个实际的基准,并且没有在每次重复计数时运行 popcnt,只有 1/10000。
@PeterCordes 我添加了ICC产生的反汇编及其伪代码,以及编译细节。
@gexicide For Coffee Lake:“POPCNT 指令的执行时间可能比预期的要长”intel.com/content/dam/www/public/us/en/documents/…
@gexicide:在 Skylake 上修复了 lzcnt/tzcnt 的错误 dep。 popcnt 的错误 dep 直到 CannonLake / IceLake 才修复。 (Why does breaking the "output dependency" of LZCNT matter? 涵盖两者)。它们是相关的,因为they all run on the same execution unit。【参考方案2】:
罪魁祸首:错误的数据依赖性(编译器甚至没有意识到这一点)
在 Sandy/Ivy Bridge 和 Haswell 处理器上,指令:
popcnt src, dest
似乎对目标寄存器dest
具有错误的依赖性。即使指令只写入它,指令也会等到dest
准备好后再执行。这种错误的依赖关系(现在)被英特尔记录为勘误表HSD146 (Haswell) 和SKL029 (Skylake)
Skylake fixed this for lzcnt
and tzcnt
.
Cannon Lake(和 Ice Lake)为 popcnt
修复了此问题。bsf
/bsr
具有真正的输出依赖性:输入 = 0 时输出未修改。 (但no way to take advantage of that with intrinsics - 只有 AMD 记录了它,编译器不会公开它。)
(是的,这些指令都运行on the same execution unit)。
这种依赖关系不仅仅支持来自单个循环迭代的 4 个popcnt
s。它可以进行循环迭代,使处理器无法并行化不同的循环迭代。
unsigned
与 uint64_t
和其他调整不会直接影响问题。但它们会影响将寄存器分配给变量的寄存器分配器。
在您的情况下,速度是卡在(错误)依赖链上的直接结果,具体取决于寄存器分配器决定做什么。
13 GB/s 有一个链:popcnt
-add
-popcnt
-popcnt
→ 下一次迭代
15 GB/s 有一个链:popcnt
-add
-popcnt
-add
→ 下一次迭代
20 GB/s 有一个链:popcnt
-popcnt
→ 下一次迭代
26 GB/s 有一个链:popcnt
-popcnt
→ 下一次迭代
20 GB/s 和 26 GB/s 之间的差异似乎是间接寻址的次要伪影。无论哪种方式,一旦达到此速度,处理器就会开始遇到其他瓶颈。
为了测试这一点,我使用内联汇编绕过编译器并得到我想要的汇编。我还拆分了 count
变量以打破所有其他可能与基准测试混淆的依赖项。
结果如下:
Sandy Bridge Xeon @ 3.5 GHz:(可在底部找到完整的测试代码)
GCC 4.6.3:g++ popcnt.cpp -std=c++0x -O3 -save-temps -march=native
Ubuntu 12
不同的寄存器:18.6195 GB/s
.L4:
movq (%rbx,%rax,8), %r8
movq 8(%rbx,%rax,8), %r9
movq 16(%rbx,%rax,8), %r10
movq 24(%rbx,%rax,8), %r11
addq $4, %rax
popcnt %r8, %r8
add %r8, %rdx
popcnt %r9, %r9
add %r9, %rcx
popcnt %r10, %r10
add %r10, %rdi
popcnt %r11, %r11
add %r11, %rsi
cmpq $131072, %rax
jne .L4
相同的寄存器:8.49272 GB/s
.L9:
movq (%rbx,%rdx,8), %r9
movq 8(%rbx,%rdx,8), %r10
movq 16(%rbx,%rdx,8), %r11
movq 24(%rbx,%rdx,8), %rbp
addq $4, %rdx
# This time reuse "rax" for all the popcnts.
popcnt %r9, %rax
add %rax, %rcx
popcnt %r10, %rax
add %rax, %rsi
popcnt %r11, %rax
add %rax, %r8
popcnt %rbp, %rax
add %rax, %rdi
cmpq $131072, %rdx
jne .L9
断链的相同寄存器:17.8869 GB/s
.L14:
movq (%rbx,%rdx,8), %r9
movq 8(%rbx,%rdx,8), %r10
movq 16(%rbx,%rdx,8), %r11
movq 24(%rbx,%rdx,8), %rbp
addq $4, %rdx
# Reuse "rax" for all the popcnts.
xor %rax, %rax # Break the cross-iteration dependency by zeroing "rax".
popcnt %r9, %rax
add %rax, %rcx
popcnt %r10, %rax
add %rax, %rsi
popcnt %r11, %rax
add %rax, %r8
popcnt %rbp, %rax
add %rax, %rdi
cmpq $131072, %rdx
jne .L14
那么编译器出了什么问题?
似乎 GCC 和 Visual Studio 都没有意识到 popcnt
有这样一个错误的依赖关系。然而,这些错误的依赖并不少见。这只是编译器是否意识到这一点的问题。
popcnt
并不是最常用的指令。因此,一个主要的编译器可能会错过这样的事情并不奇怪。似乎也没有任何文件提到这个问题。如果 Intel 不透露,那么除非有人偶然碰到它,否则外界不会知道。
(更新:As of version 4.9.2,GCC 意识到了这种错误依赖,并在启用优化时生成代码来补偿它。来自其他供应商的主要编译器,包括 Clang、MSVC 甚至 Intel 的自己的 ICC 还没有意识到这个微架构错误,并且不会发出补偿它的代码。)
为什么 CPU 会有这样的假依赖?
我们可以推测:它运行在与bsf
/ bsr
相同的执行单元上,确实有输出依赖。 (How is POPCNT implemented in hardware?)。对于这些指令,英特尔将 input=0 的整数结果记录为“未定义”(ZF=1),但英特尔硬件实际上提供了更强大的保证来避免破坏旧软件:未修改输出。 AMD 记录了这种行为。
大概是让这个执行单元的一些微指令依赖于输出,而其他微指令不依赖于输出,这在某种程度上是不方便的。
AMD 处理器似乎没有这种错误的依赖性。
完整的测试代码如下供参考:
#include <iostream>
#include <chrono>
#include <x86intrin.h>
int main(int argc, char* argv[])
using namespace std;
uint64_t size=1<<20;
uint64_t* buffer = new uint64_t[size/8];
char* charbuffer=reinterpret_cast<char*>(buffer);
for (unsigned i=0;i<size;++i) charbuffer[i]=rand()%256;
uint64_t count,duration;
chrono::time_point<chrono::system_clock> startP,endP;
uint64_t c0 = 0;
uint64_t c1 = 0;
uint64_t c2 = 0;
uint64_t c3 = 0;
startP = chrono::system_clock::now();
for( unsigned k = 0; k < 10000; k++)
for (uint64_t i=0;i<size/8;i+=4)
uint64_t r0 = buffer[i + 0];
uint64_t r1 = buffer[i + 1];
uint64_t r2 = buffer[i + 2];
uint64_t r3 = buffer[i + 3];
__asm__(
"popcnt %4, %4 \n\t"
"add %4, %0 \n\t"
"popcnt %5, %5 \n\t"
"add %5, %1 \n\t"
"popcnt %6, %6 \n\t"
"add %6, %2 \n\t"
"popcnt %7, %7 \n\t"
"add %7, %3 \n\t"
: "+r" (c0), "+r" (c1), "+r" (c2), "+r" (c3)
: "r" (r0), "r" (r1), "r" (r2), "r" (r3)
);
count = c0 + c1 + c2 + c3;
endP = chrono::system_clock::now();
duration=chrono::duration_cast<std::chrono::nanoseconds>(endP-startP).count();
cout << "No Chain\t" << count << '\t' << (duration/1.0E9) << " sec \t"
<< (10000.0*size)/(duration) << " GB/s" << endl;
uint64_t c0 = 0;
uint64_t c1 = 0;
uint64_t c2 = 0;
uint64_t c3 = 0;
startP = chrono::system_clock::now();
for( unsigned k = 0; k < 10000; k++)
for (uint64_t i=0;i<size/8;i+=4)
uint64_t r0 = buffer[i + 0];
uint64_t r1 = buffer[i + 1];
uint64_t r2 = buffer[i + 2];
uint64_t r3 = buffer[i + 3];
__asm__(
"popcnt %4, %%rax \n\t"
"add %%rax, %0 \n\t"
"popcnt %5, %%rax \n\t"
"add %%rax, %1 \n\t"
"popcnt %6, %%rax \n\t"
"add %%rax, %2 \n\t"
"popcnt %7, %%rax \n\t"
"add %%rax, %3 \n\t"
: "+r" (c0), "+r" (c1), "+r" (c2), "+r" (c3)
: "r" (r0), "r" (r1), "r" (r2), "r" (r3)
: "rax"
);
count = c0 + c1 + c2 + c3;
endP = chrono::system_clock::now();
duration=chrono::duration_cast<std::chrono::nanoseconds>(endP-startP).count();
cout << "Chain 4 \t" << count << '\t' << (duration/1.0E9) << " sec \t"
<< (10000.0*size)/(duration) << " GB/s" << endl;
uint64_t c0 = 0;
uint64_t c1 = 0;
uint64_t c2 = 0;
uint64_t c3 = 0;
startP = chrono::system_clock::now();
for( unsigned k = 0; k < 10000; k++)
for (uint64_t i=0;i<size/8;i+=4)
uint64_t r0 = buffer[i + 0];
uint64_t r1 = buffer[i + 1];
uint64_t r2 = buffer[i + 2];
uint64_t r3 = buffer[i + 3];
__asm__(
"xor %%rax, %%rax \n\t" // <--- Break the chain.
"popcnt %4, %%rax \n\t"
"add %%rax, %0 \n\t"
"popcnt %5, %%rax \n\t"
"add %%rax, %1 \n\t"
"popcnt %6, %%rax \n\t"
"add %%rax, %2 \n\t"
"popcnt %7, %%rax \n\t"
"add %%rax, %3 \n\t"
: "+r" (c0), "+r" (c1), "+r" (c2), "+r" (c3)
: "r" (r0), "r" (r1), "r" (r2), "r" (r3)
: "rax"
);
count = c0 + c1 + c2 + c3;
endP = chrono::system_clock::now();
duration=chrono::duration_cast<std::chrono::nanoseconds>(endP-startP).count();
cout << "Broken Chain\t" << count << '\t' << (duration/1.0E9) << " sec \t"
<< (10000.0*size)/(duration) << " GB/s" << endl;
free(charbuffer);
可以在此处找到同样有趣的基准测试:http://pastebin.com/kbzgL8si
此基准测试会改变(假)依赖链中 popcnt
s 的数量。
False Chain 0: 41959360000 0.57748 sec 18.1578 GB/s
False Chain 1: 41959360000 0.585398 sec 17.9122 GB/s
False Chain 2: 41959360000 0.645483 sec 16.2448 GB/s
False Chain 3: 41959360000 0.929718 sec 11.2784 GB/s
False Chain 4: 41959360000 1.23572 sec 8.48557 GB/s
【讨论】:
大家好!这里有很多过去的cmets;在离开新的之前,请review the archive。 @JustinL.看起来这个特定问题在 Clang 7.0 中已修复 @Noah 复杂寻址也会导致分层,这可以解释差异,或者只是对齐调整,总是会影响事情。 @Noah,我没有看程序集,只是这些 cmets,但似乎 所有 版本都使用索引寻址?我也可能将“间接”误读为“索引”。我不太确定间接寻址的 OP 是什么意思。尽管如此,为了回答您的问题,unlamination 可能很重要的一种常见方式是每次访问会导致额外的 1 跳,而事先设置地址可能总共只有 1 uop。例如。在 4x 展开循环中,您可以通过使用 1 uop 来计算地址,然后使用 base + offset 寻址 4 次而不是索引,从而节省 3 uop。 是的,我指的是你在中间重命名时保存的 uop,这是一个重要的瓶颈,因为它是最窄的(也就是说,这就是英特尔芯片“4 宽”的原因)。抱歉,如果我不清楚,我并不是说它可以在执行时以某种方式避免加载操作本身(始终需要 p23 uop,问题是它是否在早期阶段融合以及融合多长时间)。 @诺亚【参考方案3】:TL;DR:改用 __builtin
内在函数;他们可能会提供帮助。
我能够使gcc
4.8.4(甚至 gcc.godbolt.org 上的 4.7.3)通过使用使用相同汇编指令的__builtin_popcountll
生成最佳代码,但很幸运并且碰巧由于错误的依赖错误,制作没有意外长循环携带依赖的代码。
我不能 100% 确定我的基准测试代码,但 objdump
的输出似乎与我的观点一致。我使用了一些其他技巧(++i
vs i++
)让编译器在没有任何movl
指令的情况下为我展开循环(我必须说奇怪的行为)。
结果:
Count: 20318230000 Elapsed: 0.411156 seconds Speed: 25.503118 GB/s
基准代码:
#include <stdint.h>
#include <stddef.h>
#include <time.h>
#include <stdio.h>
#include <stdlib.h>
uint64_t builtin_popcnt(const uint64_t* buf, size_t len)
uint64_t cnt = 0;
for(size_t i = 0; i < len; ++i)
cnt += __builtin_popcountll(buf[i]);
return cnt;
int main(int argc, char** argv)
if(argc != 2)
printf("Usage: %s <buffer size in MB>\n", argv[0]);
return -1;
uint64_t size = atol(argv[1]) << 20;
uint64_t* buffer = (uint64_t*)malloc((size/8)*sizeof(*buffer));
// Spoil copy-on-write memory allocation on *nix
for (size_t i = 0; i < (size / 8); i++)
buffer[i] = random();
uint64_t count = 0;
clock_t tic = clock();
for(size_t i = 0; i < 10000; ++i)
count += builtin_popcnt(buffer, size/8);
clock_t toc = clock();
printf("Count: %lu\tElapsed: %f seconds\tSpeed: %f GB/s\n", count, (double)(toc - tic) / CLOCKS_PER_SEC, ((10000.0*size)/(((double)(toc - tic)*1e+9) / CLOCKS_PER_SEC)));
return 0;
编译选项:
gcc --std=gnu99 -mpopcnt -O3 -funroll-loops -march=native bench.c -o bench
GCC 版本:
gcc (Ubuntu 4.8.4-2ubuntu1~14.04.1) 4.8.4
Linux内核版本:
3.19.0-58-generic
CPU 信息:
processor : 0
vendor_id : GenuineIntel
cpu family : 6
model : 70
model name : Intel(R) Core(TM) i7-4870HQ CPU @ 2.50 GHz
stepping : 1
microcode : 0xf
cpu MHz : 2494.226
cache size : 6144 KB
physical id : 0
siblings : 1
core id : 0
cpu cores : 1
apicid : 0
initial apicid : 0
fpu : yes
fpu_exception : yes
cpuid level : 13
wp : yes
flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ss ht syscall nx rdtscp lm constant_tsc nopl xtopology nonstop_tsc eagerfpu pni pclmulqdq ssse3 fma cx16 pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand hypervisor lahf_lm abm arat pln pts dtherm fsgsbase tsc_adjust bmi1 hle avx2 smep bmi2 invpcid xsaveopt
bugs :
bogomips : 4988.45
clflush size : 64
cache_alignment : 64
address sizes : 36 bits physical, 48 bits virtual
power management:
【讨论】:
幸运的是,-funroll-loops
恰好在 popcnt
的错误 dep 创建的循环承载依赖链上生成了不会瓶颈的代码。使用不知道错误依赖的旧编译器版本是有风险的。如果没有-funroll-loops
,gcc 4.8.5 的循环将成为 popcnt 延迟而不是吞吐量的瓶颈,because it counts into rdx
。同样的代码,compiled by gcc 4.9.3 添加了一个xor edx,edx
来打破依赖链。
使用旧的编译器,您的代码仍然容易受到 OP 所经历的完全相同的性能变化的影响:看似微不足道的更改可能会使 gcc 变慢,因为它不知道会导致问题。 在旧的编译器上找到在一种情况下可以工作的东西是不是的问题。
记录在案,x86intrin.h
的 _mm_popcnt_*
在 GCC are forcibly inlined wrappers around the __builtin_popcount*
上起作用;内联应该使一个完全等同于另一个。我非常怀疑您是否会看到在它们之间切换可能导致的任何差异。【参考方案4】:
好的,我想对 OP 提出的一个子问题提供一个小答案,这些子问题似乎没有在现有问题中得到解决。需要注意的是,我没有进行任何测试或代码生成或反汇编,只是想分享一个想法供其他人可能解释。
为什么static
会改变性能?
有问题的行:
uint64_t size = atol(argv[1])<<20;
简答
我会查看为访问size
而生成的程序集,看看非静态版本是否涉及额外的指针间接步骤。
长答案
由于变量只有一个副本,无论它是否声明为static
,并且大小没有改变,我推测不同之处在于用于支持变量的内存位置以及它的位置在后面的代码中使用。
好的,首先,请记住,函数的所有局部变量(连同参数)都在堆栈上提供空间用于存储。现在,很明显, main() 的堆栈帧永远不会清理并且只生成一次。好的,让它static
怎么样?好吧,在这种情况下,编译器知道在进程的全局数据空间中保留空间,因此无法通过删除堆栈帧来清除该位置。但是,我们只有一个位置,那有什么区别呢?我怀疑它与如何引用堆栈上的内存位置有关。
当编译器生成符号表时,它只是为标签创建一个条目以及相关属性,如大小等。它知道它必须在内存中保留适当的空间,但实际上并没有选择那个位置,直到在进行活性分析和可能的寄存器分配之后的过程中稍晚一些。那么链接器如何知道为最终汇编代码提供给机器代码的地址呢?它要么知道最终位置,要么知道如何到达该位置。使用堆栈,非常简单地引用基于两个元素的位置,指向堆栈帧的指针,然后是帧的偏移量。这基本上是因为链接器在运行前无法知道堆栈帧的位置。
【讨论】:
在我看来更有可能使用static
碰巧改变了函数的寄存器分配,从而影响了 OP 正在测试的 Intel CPU 上popcnt
的错误输出依赖性,使用不知道要避免它们的编译器。 (因为尚未发现 Intel CPU 中的这个性能坑。)编译器可以将 static
局部变量保存在寄存器中,就像自动存储变量一样,但如果它们不优化假设 main
只运行一次,那么它将影响代码生成(因为该值仅由第一次调用设置。)
无论如何,[RIP + rel32]
和[rsp + 42]
寻址模式之间的性能差异在大多数情况下可以忽略不计。 cmp dword [RIP+rel32], immediate
不能微融合成单个负载+cmp uop,但我认为这不会是一个因素。就像我说的,在循环内部它可能无论如何都保留在寄存器中,但是调整 C++ 可能意味着不同的编译器选择。【参考方案5】:
首先,尝试估计峰值性能 - 检查 https://www.intel.com/content/dam/www/public/us/en/documents/manuals/64-ia-32-architectures-optimization-manual.pdf,特别是附录 C。
在您的情况下,表 C-10 显示 POPCNT 指令的延迟 = 3 个时钟,吞吐量 = 1 个时钟。吞吐量以时钟为单位显示您的最大速率(在 popcnt64 的情况下乘以核心频率和 8 字节以获得最佳带宽数)。
现在检查编译器做了什么并总结循环中所有其他指令的吞吐量。这将为生成的代码提供最佳估计。
最后,查看循环中指令之间的数据依赖关系,因为它们会强制延迟——大延迟而不是吞吐量——因此,在数据流链上拆分单次迭代的指令并计算它们之间的延迟,然后天真地从中获取最大值。考虑到数据流依赖性,它会给出粗略的估计。
但是,在您的情况下,只需以正确的方式编写代码即可消除所有这些复杂性。与其累加到同一个计数变量,不如累加到不同的变量(如count0、count1、...count8)并在最后将它们相加。或者甚至创建一个 counts[8] 数组并累积到其元素 - 也许,它甚至会被矢量化,您将获得更好的吞吐量。
附:并且永远不要运行基准测试,首先预热核心然后运行循环至少 10 秒或更好的 100 秒。否则,您将在硬件中测试电源管理固件和 DVFS 实现 :)
附言我听到了关于基准测试应该运行多少时间的无休止的争论。大多数最聪明的人甚至会问为什么 10 秒而不是 11 或 12 秒。我应该承认这在理论上很有趣。在实践中,您只需连续运行数百次基准测试并记录偏差。这是有趣。大多数人确实会在此之后更改源并运行 bench 以获取新的性能记录。做正确的事。
还不相信?只需通过 assp1r1n3 (https://***.com/a/37026212/9706746) 使用上述 C 版本的基准测试,然后在重试循环中尝试 100 而不是 10000。
我的 7960X 显示,RETRY=100:
计数:203182300 经过:0.008385 秒速度:12.505379 GB/s
计数:203182300 经过:0.011063 秒速度:9.478225 GB/s
计数:203182300 经过:0.011188 秒速度:9.372327 GB/s
计数:203182300 经过:0.010393 秒速度:10.089252 GB/s
计数:203182300 经过:0.009076 秒速度:11.553283 GB/s
重试=10000:
计数:20318230000 经过:0.661791 秒速度:15.844519 GB/s
计数:20318230000 经过:0.665422 秒速度:15.758060 GB/s
计数:20318230000 经过:0.660983 秒速度:15.863888 GB/s
计数:20318230000 经过:0.665337 秒速度:15.760073 GB/s
计数:20318230000 经过:0.662138 秒速度:15.836215 GB/s
P.P.P.S. 最后,关于“接受的答案”和其他谜团;-)
让我们使用 assp1r1n3 的答案 - 他有 2.5Ghz 核心。 POPCNT 有 1 个时钟吞吐量,他的代码使用 64 位 popcnt。 所以他的设置算术是 2.5Ghz * 1 时钟 * 8 字节 = 20 GB/s。 他看到了 25Gb/s,可能是由于涡轮增压到 3Ghz 左右。
因此访问 ark.intel.com 并查找 i7-4870HQ: https://ark.intel.com/products/83504/Intel-Core-i7-4870HQ-Processor-6M-Cache-up-to-3-70-GHz-?q=i7-4870HQ
该内核可运行高达 3.7Ghz,其硬件的实际最大速率为 29.6 GB/s。那么另一个 4GB/s 在哪里呢?也许,它在每次迭代中都花在循环逻辑和其他周边代码上。
现在在哪里这个错误的依赖?硬件几乎以最高速度运行。 也许我的数学不好,有时会发生:)
P.P.P.P.P.S.仍然有人建议硬件勘误是罪魁祸首,所以我按照建议创建了内联 asm 示例,见下文。
在我的 7960X 上,第一个版本(单输出到 cnt0)以 11MB/s 的速度运行, 第二个版本(输出到 cnt0、cnt1、cnt2 和 cnt3)以 33MB/s 的速度运行。 有人可以说——瞧!它是输出依赖。
好吧,也许,我的意思是写这样的代码没有意义,它不是输出依赖问题,而是愚蠢的代码生成。我们不是在测试硬件,而是在编写代码以发挥最大性能。您可以期望 HW OOO 应该重命名并隐藏那些“输出依赖项”,但是,天哪,只要做正确的事情,您将永远不会面临任何谜团。
uint64_t builtin_popcnt1a(const uint64_t* buf, size_t len)
uint64_t cnt0, cnt1, cnt2, cnt3;
cnt0 = cnt1 = cnt2 = cnt3 = 0;
uint64_t val = buf[0];
#if 0
__asm__ __volatile__ (
"1:\n\t"
"popcnt %2, %1\n\t"
"popcnt %2, %1\n\t"
"popcnt %2, %1\n\t"
"popcnt %2, %1\n\t"
"subq $4, %0\n\t"
"jnz 1b\n\t"
: "+q" (len), "=q" (cnt0)
: "q" (val)
:
);
#else
__asm__ __volatile__ (
"1:\n\t"
"popcnt %5, %1\n\t"
"popcnt %5, %2\n\t"
"popcnt %5, %3\n\t"
"popcnt %5, %4\n\t"
"subq $4, %0\n\t"
"jnz 1b\n\t"
: "+q" (len), "=q" (cnt0), "=q" (cnt1), "=q" (cnt2), "=q" (cnt3)
: "q" (val)
:
);
#endif
return cnt0;
【讨论】:
如果您以核心时钟周期(而不是秒)为单位计时,那么 1 秒对于 CPU 密集型循环来说已经足够了。即使是 100 毫秒也适用于发现主要差异或检查性能计数器的 uop 计数。尤其是在 Skylake 上,硬件 P 状态管理让它在加载开始后以微秒的时间加速到最大时钟速度。 clang 可以使用 AVX2vpshufb
自动矢量化 __builtin_popcountl
,并且不需要 C 源代码中的多个累加器来执行此操作。我不确定_mm_popcnt_u64
;可能只能使用 AVX512-VPOPCNT 自动矢量化。 (见Counting 1 bits (population count) on large data using AVX-512 or AVX-2/)
但是无论如何,查看英特尔的优化手册并没有帮助:正如公认的答案所示,问题是popcnt
的意外输出依赖性。这在英特尔最近的一些微架构的勘误表中有记录,但我认为当时还没有。如果存在意外的错误依赖项,您的 dep-chain 分析将失败,因此此答案是很好的通用建议,但不适用于此处。
你在开玩笑吗?我不必“相信”我可以在手写 asm 循环中使用性能计数器通过实验测量的东西。它们只是事实。我已经测试过了,Skylake 修复了 lzcnt
/ tzcnt
的错误依赖,但不是 popcnt
。请参阅intel.com/content/dam/www/public/us/en/documents/… 中的英特尔勘误表 SKL029。此外,gcc.gnu.org/bugzilla/show_bug.cgi?id=62011 是“已解决固定”,而不是“无效”。您声称硬件中没有输出依赖性是没有根据的。
如果你创建一个像popcnt eax, edx
/ dec ecx / jnz
这样的简单循环,你会期望它以每时钟1 次运行,在popcnt 吞吐量和take-branch 吞吐量上成为瓶颈。但它实际上仅以每 3 个时钟运行 1 个时钟,在 popcnt
延迟上重复覆盖 EAX,即使您希望它是只写的。你有一个 Skylake,所以你可以自己试试。【参考方案6】:
我用Visual Studio 2013 Express 尝试了这个,使用指针而不是索引,这稍微加快了进程。我怀疑这是因为寻址是偏移量+寄存器,而不是偏移量+寄存器+(寄存器
uint64_t* bfrend = buffer+(size/8);
uint64_t* bfrptr;
// ...
startP = chrono::system_clock::now();
count = 0;
for (unsigned k = 0; k < 10000; k++)
// Tight unrolled loop with uint64_t
for (bfrptr = buffer; bfrptr < bfrend;)
count += __popcnt64(*bfrptr++);
count += __popcnt64(*bfrptr++);
count += __popcnt64(*bfrptr++);
count += __popcnt64(*bfrptr++);
endP = chrono::system_clock::now();
duration = chrono::duration_cast<std::chrono::nanoseconds>(endP-startP).count();
cout << "uint64_t\t" << count << '\t' << (duration/1.0E9) << " sec \t"
<< (10000.0*size)/(duration) << " GB/s" << endl;
汇编代码:r10 = bfrptr, r15 = bfrend, rsi = count, rdi = buffer, r13 = k :
$LL5@main:
mov r10, rdi
cmp rdi, r15
jae SHORT $LN4@main
npad 4
$LL2@main:
mov rax, QWORD PTR [r10+24]
mov rcx, QWORD PTR [r10+16]
mov r8, QWORD PTR [r10+8]
mov r9, QWORD PTR [r10]
popcnt rdx, rax
popcnt rax, rcx
add rdx, rax
popcnt rax, r8
add r10, 32
add rdx, rax
popcnt rax, r9
add rsi, rax
add rsi, rdx
cmp r10, r15
jb SHORT $LL2@main
$LN4@main:
dec r13
jne SHORT $LL5@main
【讨论】:
【参考方案7】:您是否尝试过将-funroll-loops -fprefetch-loop-arrays
传递给 GCC?
通过这些额外的优化,我得到以下结果:
[1829] /tmp/so_25078285 $ cat /proc/cpuinfo |grep CPU|head -n1
model name : Intel(R) Core(TM) i3-3225 CPU @ 3.30GHz
[1829] /tmp/so_25078285 $ g++ --version|head -n1
g++ (Ubuntu/Linaro 4.7.3-1ubuntu1) 4.7.3
[1829] /tmp/so_25078285 $ g++ -O3 -march=native -std=c++11 test.cpp -o test_o3
[1829] /tmp/so_25078285 $ g++ -O3 -march=native -funroll-loops -fprefetch-loop-arrays -std=c++11 test.cpp -o test_o3_unroll_loops__and__prefetch_loop_arrays
[1829] /tmp/so_25078285 $ ./test_o3 1
unsigned 41959360000 0.595 sec 17.6231 GB/s
uint64_t 41959360000 0.898626 sec 11.6687 GB/s
[1829] /tmp/so_25078285 $ ./test_o3_unroll_loops__and__prefetch_loop_arrays 1
unsigned 41959360000 0.618222 sec 16.9612 GB/s
uint64_t 41959360000 0.407304 sec 25.7443 GB/s
【讨论】:
但是,您的结果仍然很奇怪(首先 unsigned 更快,然后 uint64_t 更快),因为展开并不能解决错误依赖的主要问题。【参考方案8】:我无法给出权威答案,但提供可能原因的概述。 This reference 非常清楚地表明,对于循环主体中的指令,延迟和吞吐量之间的比率为 3:1。它还显示了多次调度的效果。由于现代 x86 处理器中有(给予或接受)三个整数单元,因此通常每个周期可以分派三个指令。
因此,在峰值管道和多次调度性能以及这些机制的故障之间,我们的性能是六倍。众所周知,x86 指令集的复杂性使得奇怪的破坏很容易发生。上面的文档有一个很好的例子:
奔腾 4 的 64 位右移性能真的很差。 64 位左移以及所有 32 位移位都具有可接受的性能。看来ALU的高32位到低32位的数据通路设计得不好。
我个人遇到了一个奇怪的情况,即热循环在四核芯片的特定核心上运行得相当慢(如果我记得是 AMD)。实际上,通过关闭该核心,我们在 map-reduce 计算上获得了更好的性能。
我的猜测是整数单位的争用:popcnt
、循环计数器和地址计算都只能勉强用 32 位宽的计数器全速运行,但 64 位计数器会导致争用和流水线摊位。由于每个循环体执行总共只有大约 12 个周期,可能有 4 个具有多次分派的周期,因此单个停顿可能会合理地影响运行时间 2 倍。
使用静态变量引起的变化(我猜这只会导致指令的轻微重新排序)是 32 位代码处于争用临界点的另一个线索。
我知道这不是一个严谨的分析,但它是一个似是而非的解释。
【讨论】:
不幸的是,自从(Core 2?)以来,除了乘法/除法之外,32 位和 64 位整数运算之间几乎没有性能差异 - 此代码中不存在。跨度> @Gene:请注意,all 版本将大小存储在寄存器中,并且永远不会在循环中从堆栈中读取它。因此,地址计算不能混在一起,至少不能在循环内。 @Gene:确实很有趣的解释!但它没有解释 WTF 的主要观点:由于管道停顿,64 位比 32 位慢是一回事。但如果是这种情况,64 位版本不应该可靠比 32 位版本慢吗?相反,即使是 32 位版本,在使用 compile-time-constant 缓冲区大小时,三个不同的编译器也会发出慢代码;将缓冲区大小再次更改为静态会完全改变事情。甚至在我的同事机器上(在 Calvin 的回答中)有一个案例,其中 64 位版本要快得多!这似乎是绝对不可预测的.. @Mysticial 这就是我的观点。当 IU、总线时间等的争用为零时,没有峰值性能差异。参考资料清楚地表明了这一点。竞争使一切都不同。以下是英特尔酷睿文献中的一个示例:“设计中包含的一项新技术是 Macro-Ops Fusion,它将两条 x86 指令组合成一个微操作。例如,比较常见的代码序列,然后是条件跳转将成为单个微操作。不幸的是,该技术不适用于 64 位模式。所以我们的执行速度是 2:1。 @gexicide 我明白你在说什么,但你推断的比我的意思要多。我是说运行最快的代码是保持管道和调度队列满。这种情况是脆弱的。像向总数据流中添加 32 位和指令重新排序这样的微小更改足以破坏它。简而言之,OP 关于摆弄和测试是前进的唯一途径的断言是正确的。【参考方案9】:这不是答案,但如果我将结果放在评论中,则很难阅读。
我使用Mac Pro(Westmere 6-Cores Xeon 3.33 GHz)得到这些结果。我用clang -O3 -msse4 -lstdc++ a.cpp -o a
编译它(-O2 得到相同的结果)。
叮当uint64_t size=atol(argv[1])<<20;
unsigned 41950110000 0.811198 sec 12.9263 GB/s
uint64_t 41950110000 0.622884 sec 16.8342 GB/s
叮当uint64_t size=1<<20;
unsigned 41950110000 0.623406 sec 16.8201 GB/s
uint64_t 41950110000 0.623685 sec 16.8126 GB/s
我也尝试过:
-
颠倒测试顺序,结果相同,排除缓存因素。
将
for
语句反向:for (uint64_t i=size/8;i>0;i-=4)
。这给出了相同的结果,并证明编译足够智能,不会在每次迭代时将大小除以 8(如预期的那样)。
这是我的猜测:
速度因素分为三个部分:
代码缓存:uint64_t
版本具有更大的代码大小,但这对我的 Xeon CPU 没有影响。这会使 64 位版本变慢。
使用的说明。不仅要注意循环计数,还要注意在这两个版本上使用 32 位和 64 位索引访问缓冲区。访问具有 64 位偏移量的指针需要专用的 64 位寄存器和寻址,而您可以将立即数用于 32 位偏移量。这可能会使 32 位版本更快。
指令仅在 64 位编译(即预取)时发出。这使得 64 位更快。
这三个因素与观察到的看似矛盾的结果相匹配。
【讨论】:
有趣,可以添加编译器版本和编译器标志吗? 最好的是,在你的机器上,结果是反过来的,即使用 u64 更快。到现在为止,我从来没有想过我的循环变量是哪种类型,但看来我下次必须三思而后行:)。 @gexicide:我不会把从 16.8201 到 16.8126 的跳跃称为“更快”。 @Mehrdad:我的意思是12.9
和16.8
之间的跳跃,所以unsigned
在这里更快。在我的基准测试中,情况正好相反,即unsigned
为 26,uint64_t
为 15
@gexicide 你注意到缓冲区[i]寻址的不同了吗?
@Calvin:不,你是什么意思?【参考方案10】:
我编写了一个等效的 C 程序进行实验,我可以确认这种奇怪的行为。更重要的是,gcc
认为 64 位整数(无论如何应该是 size_t
...)会更好,因为使用 uint_fast32_t
会导致 gcc 使用 64 位 uint。
我对组件做了一些处理:
只需使用 32 位版本,在程序的内部 popcount-loop 中将所有 32 位指令/寄存器替换为 64 位版本。观察:代码与 32 位版本一样快!
这显然是一个 hack,因为变量的大小并不是真正的 64 位,因为程序的其他部分仍然使用 32 位版本,但只要内部 popcount-loop 支配性能,这是一个好的开始.
然后我从程序的 32 位版本中复制了内部循环代码,将其修改为 64 位,修改了寄存器以使其替代 64 位版本的内部循环。 此代码的运行速度也与 32 位版本一样快。
我的结论是,这是编译器的不良指令调度,而不是 32 位指令的实际速度/延迟优势。(警告:我破解了程序集,可能在没有注意到的情况下破坏了某些东西。我没有这么想。)
【讨论】:
“更重要的是,gcc 认为 64 位整数 [...] 更好,因为使用 uint_fast32_t 会导致 gcc 使用 64 位 uint。”不幸的是,令我遗憾的是,这些类型背后没有魔法,也没有深入的代码自省。除了为整个平台上的每个可能的地方和每个程序提供单一类型定义之外,我还没有看到它们以任何其他方式提供。可能已经对类型的确切选择进行了相当多的思考,但是对它们中的每一个的一个定义不可能适合所有的应用程序。进一步阅读:***.com/q/4116297. @Keno 那是因为sizeof(uint_fast32_t)
必须被定义。如果你不允许它,你可以做那个诡计,但这只能通过编译器扩展来完成。【参考方案11】:
您是否尝试过将归约步骤移到循环之外?现在你有一个真正不需要的数据依赖。
试试:
uint64_t subset_counts[4] = ;
for( unsigned k = 0; k < 10000; k++)
// Tight unrolled loop with unsigned
unsigned i=0;
while (i < size/8)
subset_counts[0] += _mm_popcnt_u64(buffer[i]);
subset_counts[1] += _mm_popcnt_u64(buffer[i+1]);
subset_counts[2] += _mm_popcnt_u64(buffer[i+2]);
subset_counts[3] += _mm_popcnt_u64(buffer[i+3]);
i += 4;
count = subset_counts[0] + subset_counts[1] + subset_counts[2] + subset_counts[3];
你还有一些奇怪的别名,我不确定是否符合严格的别名规则。
【讨论】:
这是我读完问题后做的第一件事。打破依赖链。事实证明,性能差异并没有改变(至少在我的计算机上 - Intel Haswell with GCC 4.7.3)。 @BenVoigt:它符合严格的别名。void*
和 char*
是可能有别名的两种类型,因为它们本质上被认为是“指向某个内存块的指针”!您关于删除数据依赖项的想法对优化很有帮助,但它不能回答问题。而且,正如@NilsPipenbrinck 所说,它似乎并没有改变任何东西。
@gexicide:严格的别名规则不是对称的。您可以使用char*
访问T[]
。您不能安全地使用T*
来访问char[]
,而您的代码似乎是后者。
@BenVoigt:那么您将永远无法保存 malloc
任何数组,因为 malloc 返回 void*
并且您将其解释为 T[]
。而且我很确定void*
和char*
在严格别名方面具有相同的语义。但是,我想这在这里很离题:)
个人认为正确的做法是uint64_t* buffer = new uint64_t[size/8]; /* type is clearly uint64_t[] */ char* charbuffer=reinterpret_cast<char*>(buffer); /* aliasing a uint64_t[] with char* is safe */
以上是关于用 64 位替换 32 位循环计数器会在 Intel CPU 上使用 _mm_popcnt_u64 引入疯狂的性能偏差的主要内容,如果未能解决你的问题,请参考以下文章