我在寻找popcount大型数据数组的最快方法。我遇到了一个非常奇怪的效果:将循环变量从unsigned更改为uint64\u t使我的电脑性能下降了50%
基准
#包括<;iostream>;
#包括<;chrono>;
#包括<;x86intrin.h>;
int main(int argc,char*argv[]){
使用名称空间std;
如果(argc!=2){
cerr<;<;“用法:数组大小以MB为单位”<;<;endl;
返回-1;
}
uint64_t size=atol(argv[1])<;20;
uint64_t*缓冲区=新uint64_t[大小/8];
char*charbuffer=reinterpret_cast<;char*>;(缓冲区);
用于(无符号i=0;i<;大小;++i)
charbuffer[i]=rand()%256;
uint64_t计数,持续时间;
时钟:时间点<;时钟:系统时钟>;开始,结束;
{
startP=chrono::system_clock::now();
计数=0;
用于(无符号k=0;k<;10000;k++){
//无符号紧展开循环
对于(无符号i=0;i<;大小/8;i+=4){
计数+=_mm_popcnt_u64(缓冲区[i]);
计数+=_mm_popcnt_u64(缓冲区[i+1]);
计数+=_mm_popcnt_u64(缓冲区[i+2]);
计数+=_mm_popcnt_u64(缓冲区[i+3]);
}
}
endP=chrono::system_clock::now();
duration=chrono::duration_cast<;std::chrono::纳秒>;(endP startP).count();
cout<;<;“未签名的\t”<;count<;'\t'<;<;(持续时间/1.0E9)<;<;“秒\t”
<;<;(10000.0*尺寸)/(持续时间)<;<;“GB/s”<;<;endl;
}
{
startP=chrono::system_clock::now();
计数=0;
用于(无符号k=0;k<;10000;k++){
//带uint64\U t的紧展开环
对于(uint64_t i=0;i<;大小/8;i+=4){
计数+=_mm_popcnt_u64(缓冲区[i]);
计数+=_mm_popcnt_u64(缓冲区[i+1]);
计数+=_mm_popcnt_u64(缓冲区[i+2]);
计数+=_mm_popcnt_u64(缓冲区[i+3]);
}
}
endP=chrono::system_clock::now();
duration=chrono::duration_cast<;std::chrono::纳秒>;(endP startP).count();
持续时间/1.0E9秒
<;<;(10000.0*尺寸)/(持续时间)<;<;“GB/s”<;<;endl;
}
免费(charbuffer);
}
如您所见,我们创建了一个随机数据缓冲区,其大小为xMB,其中从命令行读取x。之后,我们迭代缓冲区,并使用x86popcount内部的展开版本来执行popcount。为了得到更精确的结果,我们进行了10000次popcount。我们测量popcount的时间。在大写字母中,内循环变量是无符号的,在小写字母中,内循环变量是uint64\t。我认为这应该没有什么区别,但情况恰恰相反
(绝对疯狂的)结果
我这样编译它(g++版本:Ubuntu 4.8.2-19ubuntu1):
g++-O3-march=native-std=c++11 test.cpp-o test
以下是我的Haswell Core i7-4770K [email protected]的测试结果;GHz,运行测试1(因此1 MB随机数据):
- 未签名的41959360000 0.401554秒26.113;GB/s
- uint64 t 41959360000 0.759822秒13.8003;GB/s
如您所见,uint64\u t版本的吞吐量只有未签名版本的一半!问题似乎是生成了不同的程序集,但为什么?首先,我想到了一个编译器错误,所以我尝试了clang++(Ubuntu clang版本3.4-1ubuntu3):
clang++-O3-march=native-std=c++11 teest.cpp-o测试
结果:测试1
- 未签名41959360000 0.398293秒26.3267 GB/s
- uint64_t 41959360000 0.680954秒15.3986 GB/s
因此,这几乎是相同的结果,仍然很奇怪但现在它变得非常奇怪。我将从输入读取的缓冲区大小替换为常量1,因此我更改:
uint64\u t size=atol(argv[1])<&书信电报;20;
到
uint64\u t size=1<&书信电报;20;
因此,编译器现在知道编译时的缓冲区大小。也许它可以添加一些优化!以下是g++的编号:
- 未签名41959360000 0.509156秒20.5944;GB/s
- uint64 t 41959360000 0.508673秒20.6139;GB/s
现在,两个版本的速度都一样快。然而,未签名的变得更慢了!它从26下降到20 GB/s,因此将非常量替换为常量会导致去优化。说真的,我不知道这里发生了什么!但现在,请看新版本的clang++:
- 未签名的41959360000 0.677009秒15.4884;GB/s
- uint64 t 41959360000 0.676909秒15.4906;GB/s
等等,什么?现在,两个版本都降到了慢的15个;GB/s。因此,用常量值替换一个非常量甚至会导致在和两种情况下的代码运行缓慢
我请一位拥有常春藤桥CPU的同事编译我的基准测试。他也得到了类似的结果,所以看起来不太好。因为这里有两个编译器产生奇怪的结果,所以它似乎也不是一个编译器错误。我们这里没有AMD CPU,所以我们只能用Intel进行测试
请再疯狂一点
以第一个示例(带有atol(argv[1]))的示例)为例,将static放在变量前面,即:
静态uint64\u t size=atol(argv[1])<&书信电报;20;
以下是我在g++中的结果:
- 未签名41959360000 0.396728秒26.4306 GB/s
- uint64_t 41959360000 0.509484秒20.5811 GB/s
是啊,还有另一种选择。我们还有最快的26;使用u32的GB/s,但我们设法至少从13个;GB/s至20;GB/s版本在我的同事的PC上,u64版本比u32版本速度更快,产生了最快的结果。遗憾的是,这只适用于g++,clang++似乎并不关心静态
我的问题
你能解释一下这些结果吗?特别是:
u32和u64之间怎么会有这样的区别- 如何将非常量替换为常量缓冲区大小触发较差的代码
- 插入
static关键字如何使u64循环更快?比我同事电脑上的原始代码还要快
我知道优化是一个棘手的领域,但是,我从来没有想到如此小的变化会导致执行时间100%的差异,而像恒定缓冲区大小这样的小因素会再次完全混合结果。当然,我一直希望有一个能够流行26的版本;GB/s。我能想到的唯一可靠的方法是复制粘贴本例中的程序集并使用内联程序集。这是我摆脱编译器的唯一方法,这些编译器似乎对一些小的改动很着迷。你怎么认为?有没有其他方法可以可靠地获得性能最好的代码
拆卸
以下是各种结果的分解:
26;g++/u32/non const bufsize的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
添加%r9,%rax
popcnt(%rbx,%rdi,8),%rcx
添加$0x4,%edx
添加%rcx,%rax
popcnt(%rbx,%rsi,8),%rcx
添加%rcx,%rax
移动%edx,%ecx
添加%rax,%r14
cmp%rbp,%rcx
jb 0x400af8
13;g++/u64/non const bufsize的GB/s版本
0x400c00:
popcnt 0x8(%rbx,%rdx,8),%rcx
popcnt(%rbx,%rdx,8),%rax
添加%rcx,%rax
popcnt 0x10(%rbx,%rdx,8),%rcx
添加%rcx,%rax
popcnt 0x18(%rbx,%rdx,8),%rcx
添加$0x4,%rdx
添加%rcx,%rax
添加%rax,%r12
cmp%rbp,%rdx
jb 0x400c00
15;GB/s版本,来自clang++/u64/non-const bufsize:
0x400e50:
popcnt(%r15,%rcx,8),%rdx
添加%rbx,%rdx
popcnt 0x8(%r15,%rcx,8),%rsi
添加%rdx,%rsi
popcnt 0x10(%r15,%rcx,8),%rdx
添加%rsi,%rdx
popcnt 0x18(%r15,%rcx,8),%rbx
添加%rdx,%rbx
添加$0x4,%rcx
cmp%rbp,%rcx
jb 0x400e50
20;g++/u32&;的GB/s版本;u64/const bufsize:
0x400a68:
popcnt(%rbx,%rdx,1),%rax
popcnt 0x8(%rbx,%rdx,1),%rcx
添加%rax,%rcx
popcnt 0x10(%rbx,%rdx,1),%rax
添加%rax,%rcx
popcnt 0x18(%rbx,%rdx,1),%rsi
添加$0x20,%rdx
添加%rsi,%rcx
添加%rcx,%rbp
cmp$0x100000,%rdx
jne 0x400a68
15;来自clang++/u32&;的GB/s版本;u64/const bufsize:
0x400dd0:
popcnt(%r14,%rcx,8),%rdx
添加%rbx,%rdx
popcnt 0x8(%r14,%rcx,8),%rsi
添加%rdx,%rsi
popcnt 0x10(%r14,%rcx,8),%rdx
添加%rsi,%rdx
popcnt 0x18(%r14,%rcx,8),%rbx
添加%rdx,%rbx
添加$0x4,%rcx
cmp$0x20000,%rcx
jb 0x400dd0
有趣的是,最快(26 GB/s)的版本也是最长的!这似乎是使用lea的唯一解决方案。一些版本使用jb跳转,其他版本使用jne。但除此之外,所有版本似乎都具有可比性。我不知道100%的性能差距是从何而来的,但我不太擅长破译程序集。最慢(13 GB/s)的版本看起来甚至很短也很好。有人能解释一下吗
经验教训
不管这个问题的答案是什么;我有