用64位替换32位循环计数器会导致英特尔CPU上_mm_popcnt_u64的疯狂性能偏差

我正在寻找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 unsignedfor (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_tfor (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。之后,我们迭代缓冲区,并使用x86popcount内部的展开版本来执行popcoun。为了得到更精确的结果,我们做了10,000次popcoun。我们测量popcoun的时间。在大写情况下,内循环变量是unsigned,在小写情况下,内循环变量是uint64_t。我认为这应该没有区别,但情况恰恰相反。

(绝对疯狂的)结果

我这样编译(g++版本:Ubuntu 4.8.2-19ubuntu1):

g++ -O3 -march=native -std=c++11 test.cpp -o test

以下是我的HaswellCore i7-4770K CPU@3.50 GHz的结果,运行test 1(所以1 MB随机数据):

  • 无符号419593600000.401554 sec26.113 GB/s
  • uint64_t419593600000.759822秒13.8003 GB/s

如你所见,uint64_t版本的吞吐量是只有一半unsigned版本的吞吐量!问题似乎是生成了不同的程序集,但为什么呢?首先,我想到了一个编译器bug,所以我尝试了clang++(UbuntuClang版本3.4-1ubuntu3):

clang++ -O3 -march=native -std=c++11 teest.cpp -o test

结果:test 1

  • 无符号419593600000.398293 sec26.3267 GB/s
  • uint64_t419593600000.680954秒15.3986 GB/s

但现在它变得超级奇怪。我用常量1替换从输入读取的缓冲区大小,所以我改变了:

uint64_t size = atol(argv[1]) << 20;

uint64_t size = 1 << 20;

因此,编译器现在在编译时知道缓冲区大小。也许它可以添加一些优化!

  • 无符号419593600000.509156秒20.5944 GB/s
  • uint64_t419593600000.508673秒20.6139 GB/s

现在,两个版本都同样快。然而,unsigned变得更慢!它从26下降到20 GB/s,因此用常量值替换非常量导致去优化。说真的,我不知道这里发生了什么!但现在到新版本的clang++

  • 无符号419593600000.677009 sec15.4884 GB/s
  • uint64_t419593600000.676909秒15.4906 GB/s

等等,什么?现在,两个版本都降至15 GB/s的缓慢数字。因此,用常量值替换非常量甚至会导致Clang的两者情况下代码速度慢!

我请一位CPU常春藤桥的同事编译我的基准测试。他得到了类似的结果,所以似乎不是Haswell。因为两个编译器在这里产生奇怪的结果,所以它似乎也不是编译器bug。我们这里没有AMD CPU,所以我们只能用英特尔进行测试。

更多的疯狂,请!

以第一个例子(带有atol(argv[1])的那个)为例,在变量之前放一个static,即:

static uint64_t size=atol(argv[1])<<20;

以下是我在g++中的结果:

  • 无符号419593600000.396728 sec26.4306 GB/s
  • uint64_t419593600000.509484秒20.5811 GB/s

耶,又一个选择。我们仍然有u32的快速26 GB/s,但我们设法至少从13 GB/s到20 GB/s版本获得了u64在我同事的PC上,#1版本变得比#0版本更快,产生了最快的结果。可悲的是,这只适用于g++clang++似乎并不关心static

我的问题

你能解释一下这些结果吗?特别是:

  • u32u64之间怎么会有这样的区别?
  • 如何用常量缓冲区大小触发器次优代码替换非常量?
  • 插入static关键字如何使u64循环更快?甚至比我同事电脑上的原始代码还要快!

我知道优化是一个棘手的领域,然而,我从未想过如此小的更改会导致执行时间100%的差异,并且像恒定的缓冲区大小这样的小因素会再次完全混合结果。当然,我一直希望拥有能够弹出26 GB/s的版本。我能想到的唯一可靠的方法是针对这种情况复制粘贴程序集并使用内联汇编。这是我摆脱似乎在小更改上疯狂的编译器的唯一方法。你觉得呢?有没有其他方法可以可靠地获得性能最高的代码?

的拆解

以下是对各种结果的分解:

来自g++/u32/un-const bufsize自定义函数的26 GB/s版本:

0x400af8:lea 0x1(%rdx),%eaxpopcnt (%rbx,%rax,8),%r9lea 0x2(%rdx),%edipopcnt (%rbx,%rcx,8),%raxlea 0x3(%rdx),%esiadd %r9,%raxpopcnt (%rbx,%rdi,8),%rcxadd $0x4,%edxadd %rcx,%raxpopcnt (%rbx,%rsi,8),%rcxadd %rcx,%raxmov %edx,%ecxadd %rax,%r14cmp %rbp,%rcxjb 0x400af8

13 GB/s版本从g++u64/un-const bufsize自定义函数

0x400c00:popcnt 0x8(%rbx,%rdx,8),%rcxpopcnt (%rbx,%rdx,8),%raxadd %rcx,%raxpopcnt 0x10(%rbx,%rdx,8),%rcxadd %rcx,%raxpopcnt 0x18(%rbx,%rdx,8),%rcxadd $0x4,%rdxadd %rcx,%raxadd %rax,%r12cmp %rbp,%rdxjb 0x400c00

15 GB/s版本从clang++/u64/un-const bufsize核心组件配置

0x400e50:popcnt (%r15,%rcx,8),%rdxadd %rbx,%rdxpopcnt 0x8(%r15,%rcx,8),%rsiadd %rdx,%rsipopcnt 0x10(%r15,%rcx,8),%rdxadd %rsi,%rdxpopcnt 0x18(%r15,%rcx,8),%rbxadd %rdx,%rbxadd $0x4,%rcxcmp %rbp,%rcxjb 0x400e50

20 GB/s版本从g++/u32&u64/const bufsize自定义参数

0x400a68:popcnt (%rbx,%rdx,1),%raxpopcnt 0x8(%rbx,%rdx,1),%rcxadd %rax,%rcxpopcnt 0x10(%rbx,%rdx,1),%raxadd %rax,%rcxpopcnt 0x18(%rbx,%rdx,1),%rsiadd $0x20,%rdxadd %rsi,%rcxadd %rcx,%rbpcmp $0x100000,%rdxjne 0x400a68

15 GB/s版本从

0x400dd0:popcnt (%r14,%rcx,8),%rdxadd %rbx,%rdxpopcnt 0x8(%r14,%rcx,8),%rsiadd %rdx,%rsipopcnt 0x10(%r14,%rcx,8),%rdxadd %rsi,%rdxpopcnt 0x18(%r14,%rcx,8),%rbxadd %rdx,%rbxadd $0x4,%rcxcmp $0x20000,%rcxjb 0x400dd0

有趣的是,最快的(26 GB/s)版本也是最长的!它似乎是唯一使用lea的解决方案。有些版本使用jb跳转,有些版本使用jne。但除此之外,所有版本似乎都具有可比性。我不知道100%的性能差距是从哪里来的,但我不太擅长破译汇编。最慢的(13 GB/s)版本看起来甚至很短,很好。有人能解释一下吗?

吸取的教训

不管这个问题的答案是什么;我已经了解到,在真正的热循环中细节可能很重要,甚至看起来与热代码没有任何关联的细节。我从来没有想过要为循环变量使用什么类型,但是正如你所看到的那样,这样的微小变化可以产生百分之百的差异!即使是缓冲区的存储类型也可以产生巨大的差异,正如我们在大小变量前面插入static关键字所看到的那样!将来,当编写对系统性能至关重要的真正紧密和热循环时,我将始终在各种编译器上测试各种替代方案。

有趣的是,尽管我已经展开了四次循环,但性能差异仍然很高。所以即使你展开,你仍然会受到主要性能偏差的影响。非常有趣。

187113 次浏览

这不是一个答案,但如果我把结果放在评论中,很难阅读。

我用mac pro韦斯特米尔 6-CoresXeon 3.33 GHz)得到这些结果。我用clang -O3 -msse4 -lstdc++ a.cpp -o a编译它(-O2得到相同的结果)。

clanguint64_t size=atol(argv[1])<<20;

unsigned    41950110000 0.811198 sec    12.9263 GB/suint64_t    41950110000 0.622884 sec    16.8342 GB/s

clanguint64_t size=1<<20;

unsigned    41950110000 0.623406 sec    16.8201 GB/suint64_t    41950110000 0.623685 sec    16.8126 GB/s

我也试着:

  1. 颠倒测试顺序,结果是相同的,因此它排除了缓存因子。
  2. 反过来使用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位更快。

这三个因素与观察到的看似矛盾的结果相匹配。

您是否尝试过将缩减步骤移到循环之外?现在您有一个真正不需要的数据依赖项。

尝试:

  uint64_t subset_counts[4] = {};for( unsigned k = 0; k < 10000; k++){// Tight unrolled loop with unsignedunsigned 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];

你也有一些奇怪的混淆现象,我不确定是否符合严格的混淆现象规则。

我不能给出一个权威的答案,但提供一个可能原因的概述。这一参考非常清楚地表明,对于循环主体中的指令,延迟和吞吐量之间的比率为3:1。它还显示了多次分派的影响。由于现代x86处理器中有(给予或接受)三个整数单元,因此每个周期通常可以分派三个指令。

因此,在峰值流水线和多分派性能以及这些机制的故障之间,我们在性能上有六个因素。众所周知,x86指令集的复杂性使得很容易发生古怪的破坏。上面的文档有一个很好的例子:

奔腾4的64位右移性能很差。64位左移和所有32位移位都有可接受的性能。看来ALU从上32位到下32位的数据通路设计得不好。

我个人遇到过一个奇怪的情况,在四核芯片(AMD)的特定内核上,热循环运行得相当慢。通过关闭该内核,我们实际上在map-duce计算上获得了更好的性能。

这里我的猜测是整数单位的争用:popcnt、循环计数器和地址计算都只能在32位宽计数器下勉强全速运行,但是64位计数器会导致争用和管道停顿。由于总共只有大约12个周期,可能有4个周期具有多个调度,每个循环主体执行,单个停顿可能会合理地影响运行时间2倍。

使用静态变量引起的变化,我猜只是导致指令的轻微重新排序,是32位代码处于争用临界点的另一个线索。

我知道这不是一个严谨的分析,但它一个合理的解释。

罪魁祸首:虚假数据依赖(编译器甚至没有意识到它)

在Sandy/Ivy Bridge和Haswell处理器上,指令:

popcnt  src, dest

似乎对目标寄存器dest有错误的依赖关系。即使指令只写入它,指令也会等到dest准备好才执行。这种错误的依赖关系(现在)被英特尔记录为勘误表HSD146(哈斯韦尔)SKL029(Skylake)

Skylake为#0和#1修复了这个
大炮湖(和冰湖)修复了这个popcntbsf/bsr有一个真正的输出依赖关系:输入=0时输出未修改。(但是没有办法利用内在函数的优势-只有AMD记录它,编译器不会公开它。)

(是的,这些指令都运行在同一个执行单元)。


这种依赖不仅仅会占用单个循环迭代的4popcnt。它可以跨循环迭代,使处理器无法并行化不同的循环迭代。

unsigned vs.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
  • ubuntu12

不同的寄存器:18.6195 GB/s

.L4:movq    (%rbx,%rax,8), %r8movq    8(%rbx,%rax,8), %r9movq    16(%rbx,%rax,8), %r10movq    24(%rbx,%rax,8), %r11addq    $4, %rax
popcnt %r8, %r8add    %r8, %rdxpopcnt %r9, %r9add    %r9, %rcxpopcnt %r10, %r10add    %r10, %rdipopcnt %r11, %r11add    %r11, %rsi
cmpq    $131072, %raxjne .L4

相同的寄存器:8.49272 GB/s

.L9:movq    (%rbx,%rdx,8), %r9movq    8(%rbx,%rdx,8), %r10movq    16(%rbx,%rdx,8), %r11movq    24(%rbx,%rdx,8), %rbpaddq    $4, %rdx
# This time reuse "rax" for all the popcnts.popcnt %r9, %raxadd    %rax, %rcxpopcnt %r10, %raxadd    %rax, %rsipopcnt %r11, %raxadd    %rax, %r8popcnt %rbp, %raxadd    %rax, %rdi
cmpq    $131072, %rdxjne .L9

与断链相同的寄存器:17.8869 GB/s

.L14:movq    (%rbx,%rdx,8), %r9movq    8(%rbx,%rdx,8), %r10movq    16(%rbx,%rdx,8), %r11movq    24(%rbx,%rdx,8), %rbpaddq    $4, %rdx
# Reuse "rax" for all the popcnts.xor    %rax, %rax    # Break the cross-iteration dependency by zeroing "rax".popcnt %r9, %raxadd    %rax, %rcxpopcnt %r10, %raxadd    %rax, %rsipopcnt %r11, %raxadd    %rax, %r8popcnt %rbp, %raxadd    %rax, %rdi
cmpq    $131072, %rdxjne .L14

那么编译器出了什么问题?

似乎GCC和Visual Studio都不知道popcnt有这样一个错误的依赖关系。尽管如此,这些错误的依赖关系并不罕见。这只是编译器是否意识到的问题。

popcnt并不是最常用的指令。所以一个主要的编译器会错过这样的东西并不奇怪。似乎也没有任何地方提到这个问题的留档。如果英特尔不披露,那么在有人偶然遇到之前,外面的人不会知道。

更新时间:版本4.9.2,GCC意识到了这种错误依赖,并在启用优化时生成代码来补偿它。其他供应商的主要编译器,包括Clang、MSVC,甚至英特尔自己的ICC,都还不知道这种微架构错误,也不会发出补偿它的代码。)

为什么CPU有这样一个错误的依赖关系?

我们可以推测:它运行在与bsf/bsr相同的执行单元上,有输出依赖关系。(POPCNT如何在硬件中实现?)。对于这些指令,英特尔将输入=0的整数结果记录为“未定义”(ZF=1),但英特尔硬件实际上提供了更强的保证来避免破坏旧软件:输出未修改。AMD记录了这种行为。

据推测,为这个执行单元制作一些uops依赖于输出而不是其他输出是不方便的。

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
此基准更改(false)依赖链中的popcnt的数量。

False Chain 0:  41959360000 0.57748 sec     18.1578 GB/sFalse Chain 1:  41959360000 0.585398 sec    17.9122 GB/sFalse Chain 2:  41959360000 0.645483 sec    16.2448 GB/sFalse Chain 3:  41959360000 0.929718 sec    11.2784 GB/sFalse Chain 4:  41959360000 1.23572 sec     8.48557 GB/s

我编写了一个等效的C程序进行实验,我可以确认这种奇怪的行为。更重要的是,gcc认为64位整数(无论如何应该是size_t…)更好,因为使用uint_fast32_t会导致gcc使用64位uint。

我在大会上做了一点胡闹:
简单地采用32位版本,将程序内部popcount-loop中的所有32位指令/寄存器替换为64位版本。观察:代码是和32位版本一样快!

这显然是一个黑客攻击,因为变量的大小并不是真正的64位,因为程序的其他部分仍然使用32位版本,但只要内部popcount-loop主导性能,这就是一个好的开始。然后,我从程序的32位版本中复制了内部循环代码,将其破解为64位,摆弄寄存器以使其替换64位版本的内部循环。此代码的运行速度也与32位版本一样快。

我的结论是,这是编译器糟糕的指令调度,而不是32位指令的实际速度/延迟优势。

(警告:我破解了程序集,可能在没有注意到的情况下损坏了一些东西。我不这么认为。)

我用Visual Studio 2013 Express尝试了这个,使用指针而不是索引,这加快了进程。我怀疑这是因为寻址是偏移量+寄存器,而不是偏移量+寄存器+(寄存器<<3)。C++代码。

   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_tfor (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=计数,rdi=缓冲区,r13=k:

$LL5@main:mov     r10, rdicmp     rdi, r15jae     SHORT $LN4@mainnpad    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, raxpopcnt  rax, rcxadd     rdx, raxpopcnt  rax, r8add     r10, 32add     rdx, raxpopcnt  rax, r9add     rsi, raxadd     rsi, rdxcmp     r10, r15jb      SHORT $LL2@main$LN4@main:dec     r13jne     SHORT $LL5@main

你有没有尝试过将-funroll-loops -fprefetch-loop-arrays传递给GCC?

通过这些额外的优化,我得到了以下结果:

[1829] /tmp/so_25078285 $ cat /proc/cpuinfo |grep CPU|head -n1model name      : Intel(R) Core(TM) i3-3225 CPU @ 3.30GHz[1829] /tmp/so_25078285 $ g++ --version|head -n1g++ (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 1unsigned        41959360000     0.595 sec       17.6231 GB/suint64_t        41959360000     0.898626 sec    11.6687 GB/s
[1829] /tmp/so_25078285 $ ./test_o3_unroll_loops__and__prefetch_loop_arrays 1unsigned        41959360000     0.618222 sec    16.9612 GB/suint64_t        41959360000     0.407304 sec    25.7443 GB/s

太长别读:使用__builtin intrinsics代替;它们可能会有帮助。

我能够使gcc 4.8.4(甚至gcc.godbolt.org上的4.7.3)通过使用__builtin_popcountll生成最佳代码,它使用相同的汇编指令,但幸运的是,由于错误的依赖bug,碰巧使代码没有意外的长循环携带依赖关系。

我不是100%确定我的基准测试代码,但objdump输出似乎分享了我的观点。我使用了一些其他技巧(++i vsi++)让编译器在没有任何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 *nixfor (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   : 0vendor_id   : GenuineIntelcpu family  : 6model       : 70model name  : Intel(R) Core(TM) i7-4870HQ CPU @ 2.50 GHzstepping    : 1microcode   : 0xfcpu MHz     : 2494.226cache size  : 6144 KBphysical id : 0siblings    : 1core id     : 0cpu cores   : 1apicid      : 0initial apicid  : 0fpu     : yesfpu_exception   : yescpuid level : 13wp      : yesflags       : 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 xsaveoptbugs        :bogomips    : 4988.45clflush size    : 64cache_alignment : 64address sizes   : 36 bits physical, 48 bits virtualpower management:

首先,尝试估计峰值性能-检查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)并在最后对它们进行求和。或者甚至创建一个计数[8]数组并累加到它的元素——也许,它将被矢量化,你将获得更好的吞吐量。

注意:永远不要运行基准测试一秒钟,首先预热核心,然后运行循环至少10秒或更好100秒。否则,您将测试硬件中的电源管理固件和DVFS实现:)

附言:我听到了关于基准测试真正应该运行多少时间的无休止的争论。最聪明的人甚至会问为什么是10秒而不是11秒或12秒。我应该承认这在理论上很有趣。在实践中,你只需连续运行基准测试一百次并记录偏差。这is很有趣。大多数人确实会更改源代码并在那之后运行板凳一次以获得新的性能记录。做正确的事情。

还不相信?只需使用assp1r1n3(https://stackoverflow.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

如果RETRY=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个时钟通过hgput,他的代码使用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. P. S.仍然有人认为HW勘误表是罪魁祸首,所以我遵循建议并创建了内联asm示例,见下文。

在我的7960X上,第一个版本(单个输出到cnt0)以11MB/s的速度运行,第二个版本(输出为cnt0、cnt1、cnt2和cnt3)运行速度为33MB/s。有人可能会说-瞧!这是输出依赖关系。

好吧,也许,我想说的是,写这样的代码是没有意义的,这不是输出依赖关系问题,而是愚蠢的代码生成。我们不是在测试硬件,我们编写代码是为了释放最大的性能。你可以期望硬件面向对象应该重命名并隐藏那些“输出依赖关系”,但是,gash,只要做正确的事情,你就永远不会面临任何神秘。

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):);#endifreturn cnt0;}

好的,我想对OP提出的其中一个子问题提供一个小答案,这些问题在现有问题中似乎没有得到解决。警告,我没有做任何测试或代码生成,或反汇编,只是想分享一个想法,供其他人可能阐述。

为什么static会改变性能?

有问题的行:uint64_t size = atol(argv[1])<<20;

简短回答

我将查看为访问size而生成的程序集,并查看非静态版本是否涉及指针间接的额外步骤。

长答案

由于变量只有一个副本,无论它是否被声明为static,并且大小都不会改变,我认为差异在于用于返回变量的内存位置以及它在代码中的使用位置。

好的,从显而易见的开始,记住函数的所有局部变量(以及参数)都在堆栈上提供空间用作存储。现在,很明显,main()的堆栈帧永远不会清理,只生成一次。好的,把它变成static怎么样?嗯,在这种情况下,编译器知道在进程的全局数据空间中保留空间,因此不能通过删除堆栈帧来清除该位置。但是,我们只有一个位置,那么有什么区别?我怀疑这与堆栈上的内存位置是如何引用的有关。

编译器在生成符号表时,它只是为标签以及相关属性(如大小等)做一个条目。它知道它必须在内存中保留适当的空间,但实际上并没有选择该位置,直到在进行活性分析和可能的寄存器分配后的某个过程中。那么链接器如何知道为最终的汇编代码向机器代码提供什么地址?它要么知道最终位置,要么知道如何到达该位置。对于堆栈,基于两个元素引用位置很简单,指向堆栈帧的指针,然后是帧中的偏移量。这基本上是因为链接器在运行时之前无法知道堆栈框架的位置。

这不是答案,而是2021年很少编译器的反馈。英特尔咖啡湖9900k

使用Microsoft编译器(VS2019),工具集v142:

unsigned        209695540000    1.8322 sec      28.6152 GB/suint64_t        209695540000    3.08764 sec     16.9802 GB/s

使用英特尔编译器2021:

unsigned        209695540000    1.70845 sec     30.688 GB/suint64_t        209695540000    1.57956 sec     33.1921 GB/s

根据Mysticia的回答,Intel编译器知道False Data Dependency,但不知道Microsoft编译器。

对于英特尔编译器,我使用了/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; // rsiunsigned int v9; // ediunsigned __int64 i; // rbxunsigned __int64 v11; // rdiint v12; // ebp__int64 v13; // r14__int64 v14; // rbxunsigned int v15; // eaxunsigned __int64 v16; // rcxunsigned int v17; // eaxunsigned __int64 v18; // rcx__int64 v19; // rdxunsigned int v20; // eaxint result; // eaxstd::ostream *v23; // rbxchar v24; // dlstd::ostream *v33; // rbxstd::ostream *v41; // rbx__int64 v42; // rdxunsigned int v43; // eaxint v44; // ebp__int64 v45; // r14__int64 v46; // rbxunsigned __int64 v47; // raxunsigned __int64 v48; // raxstd::ostream *v50; // rdichar v51; // dlstd::ostream *v58; // rdistd::ostream *v60; // rdi__int64 v61; // rdxunsigned int v62; // eax
__asm{vmovdqa [rsp+98h+var_58], xmm8vmovapd [rsp+98h+var_68], xmm7vmovapd [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, rbpvmovdqa xmm8, cs:__xmm@00000000000000004530000043300000vpunpckldq xmm0, xmm0, xmm8vmovapd xmm7, cs:__xmm@45300000000000004330000000000000vsubpd  xmm0, xmm0, xmm7vpermilpd xmm1, xmm0, 1vaddsd  xmm6, xmm1, xmm0vdivsd  xmm1, xmm6, cs:__real@41cdcd6500000000}v33 = (std::ostream *)std::ostream::operator<<(v23);std::operator___std::char_traits_char___(v33, " sec \t");__asm{vmovq   xmm0, r15vpunpckldq xmm0, xmm0, xmm8vsubpd  xmm0, xmm0, xmm7vpermilpd xmm1, xmm0, 1vaddsd  xmm0, xmm1, xmm0vmulsd  xmm7, xmm0, cs:__real@40c3880000000000vdivsd  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, rbpvpunpckldq xmm0, xmm0, cs:__xmm@00000000000000004530000043300000vsubpd  xmm0, xmm0, cs:__xmm@45300000000000004330000000000000vpermilpd xmm1, xmm0, 1vaddsd  xmm6, xmm1, xmm0vdivsd  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;}

和拆卸主要:

.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

咖啡湖规格更新"POPCNT指令的执行时间可能比预期的要长"。