用64位替换32位循环计数器会引入疯狂的性能偏差

gex*_*ide 1370 c++ performance x86 assembly compiler-optimization

我一直在寻找最快的方法来处理popcount大数据.我遇到了一个很奇怪的效果:改变从循环变量unsigneduint64_t50%在我的电脑上所做的性能下降.

基准

#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);
}
Run Code Online (Sandbox Code Playgroud)

如您所见,我们创建一个随机数据缓冲区,其大小为x兆字节,x从命令行读取.然后,我们遍历缓冲区并使用展开的x86 popcount内部版本来执行popcount.为了获得更精确的结果,我们做了10,000次popcount.我们测量popcount的时间.在大写的情况下,内循环变量unsigned在小写的情况下是内循环变量uint64_t.我认为这应该没有区别,但事实恰恰相反.

(绝对疯狂)的结果

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

g++ -O3 -march=native -std=c++11 test.cpp -o test
Run Code Online (Sandbox Code Playgroud)

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

  • 无符号41959360000 0.401554秒 26.113 GB/s
  • uint64_t 41959360000 0.759822 sec 13.8003 GB/s

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

clang++ -O3 -march=native -std=c++11 teest.cpp -o test
Run Code Online (Sandbox Code Playgroud)

结果: test 1

  • unsigned 41959360000 0.398293 sec 26.3267 GB/s
  • uint64_t 41959360000 0.680954 sec 15.3986 GB/s

所以,它几乎是相同的结果,仍然很奇怪.但现在它变得非常奇怪.我用常量替换从输入读取的缓冲区大小1,所以我改变:

uint64_t size = atol(argv[1]) << 20;
Run Code Online (Sandbox Code Playgroud)

uint64_t size = 1 << 20;
Run Code Online (Sandbox Code Playgroud)

因此,编译器现在知道编译时的缓冲区大小.也许它可以添加一些优化!以下是数字g++:

  • unsigned 41959360000 0.509156 sec 20.5944 GB/s
  • uint64_t 41959360000 0.508673 sec 20.6139 GB/s

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

  • unsigned 41959360000 0.677009 sec 15.4884 GB/s
  • uint64_t 41959360000 0.676909 sec 15.4906 GB/s

等等,什么?现在,两个版本的速度都降至15 GB/s.因此,对于Clang来说,将两个非常数替换为常数值甚至会导致代码速度变慢!

我问一位有Ivy Bridge CPU 的同事来编译我的基准测试.他得到了类似的结果,所以它似乎不是哈斯威尔.因为这里有两个编译器产生奇怪的结果,所以它似乎也不是编译器错误.我们这里没有AMD CPU,所以我们只能用Intel测试.

请更疯狂!

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

static uint64_t size=atol(argv[1])<<20;
Run Code Online (Sandbox Code Playgroud)

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

  • 无符号41959360000 0.396728秒 26.4306 GB/s
  • uint64_t 41959360000 0.509484 sec 20.5811 GB/s

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

我的问题

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

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

我知道优化是一个棘手的领域,但是,我从未想过这么小的变化会导致执行时间的100%差异,并且像缓冲区大小一样的小因素可以再次完全混合结果.当然,我总是想拥有能够突破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
Run Code Online (Sandbox Code Playgroud)

来自g ++/u64/non-const bufsize的 13 GB/s版本:

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
Run Code Online (Sandbox Code Playgroud)

来自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
Run Code Online (Sandbox Code Playgroud)

来自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
Run Code Online (Sandbox Code Playgroud)

来自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
Run Code Online (Sandbox Code Playgroud)

有趣的是,最快的(26 GB/s)版本也是最长的版本!它似乎是唯一使用的解决方案lea.有些版本用于jb跳转,有些版本用于跳转jne.但除此之外,所有版本似乎都具有可比性.我没有看到100%的性能差距可能来自哪里,但我不太擅长破译装配.最慢的(13 GB/s)版本看起来甚至非常简短.有谁能解释一下?

得到教训

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

有趣的是,尽管我已经将循环展开了四次,但性能差异仍然很高.因此,即使您展开,您仍然会受到主要性能偏差的影响.很有趣.

Mys*_*ial 1514

罪魁祸首:虚假数据依赖(并且编译器甚至不知道它)

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

popcnt  src, dest
Run Code Online (Sandbox Code Playgroud)

似乎对目标寄存器具有错误依赖性dest.即使指令只写入它,指令也会等到dest执行前准备就绪.

这种依赖性不仅仅会阻止lzcnt单循环迭代中的4 秒.它可以进行循环迭代,使得处理器不可能并行化不同的循环迭代.

tzcntpopcnt等的调整不会直接影响的问题.但它们影响寄存器分配器,它将寄存器分配给变量.

在您的情况下,速度是固定(假)依赖链的直接结果,具体取决于寄存器分配器决定做什么.

  • 13 GB/s有一个链:bsf- bsr- popcnt- unsigned→下一次迭代
  • 15 GB/s有一个链:uint64_t- popcnt- add- popcnt→下一次迭代
  • 20 GB/s有链:popcnt- popcnt→下一次迭代
  • 26 GB/s有一个链:add- popcnt→下一次迭代

20 GB/s和26 GB/s之间的差异似乎是间接寻址的一个小工件.无论哪种方式,一旦达到此速度,处理器就会开始遇到其他瓶颈.


为了测试这个,我使用内联汇编绕过编译器并获得我想要的精确程序集.我还拆分了add变量以打破可能会破坏基准测试的所有其他依赖项.

结果如下:

Sandy Bridge Xeon @ 3.5 GHz :(完整的测试代码可以在底部找到)

  • GCC 4.6.3: popcnt
  • 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
Run Code Online (Sandbox Code Playgroud)

相同寄存器: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
Run Code Online (Sandbox Code Playgroud)

相同的寄存器断链: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
Run Code Online (Sandbox Code Playgroud)

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

似乎GCC和Visual Studio都没有意识到它popcnt具有如此错误的依赖性.然而,这些错误的依赖并不罕见.这只是编译器是否意识到它的问题.

popcnt并不是最常用的指令.所以主要的编译器可能会错过这样的东西并不奇怪.在任何地方似乎都没有提到这个问题的文件.如果英特尔没有透露它,那么外面的任何人都不会知道,直到有人碰到它.

(更新: 从版本4.9.2开始,GCC意识到这种错误依赖性并生成代码以在启用优化时对其进行补偿.来自其他供应商的主要编译器,包括Clang,MSVC,甚至是英特尔自己的ICC,还不知道这个微体系结构的错误,不会发出补偿它的代码.)

为什么CPU有这样的错误依赖?

我们只能推测,但英特尔对很多双操作数指令的处理可能相同.像普通的指令popcnt,count有两个操作数两者都输入.因此,英特尔可能会推动g++ popcnt.cpp -std=c++0x -O3 -save-temps -march=native同一类别,以保持处理器设计的简单性.

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);
}
Run Code Online (Sandbox Code Playgroud)

同样有趣的基准可以在这里找到: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
Run Code Online (Sandbox Code Playgroud)

  • 嗨伙计!这里有很多过去的评论;在留下一个新的之前,请[查看存档](http://chat.stackoverflow.com/transcript/130644/discussion-on-answer-by-mysticial-replacing-a-32-bit-loop-count-variable -with-64)。 (4认同)
  • @Noah,我没有看程序集,只是看这些注释,但似乎*所有*版本都使用索引寻址?我也可能将“间接”误读为“索引”。我不太清楚OP所说的间接寻址是什么意思。不过,为了回答您的问题,取消分层可能很重要的一种常见方式是,它会导致每次访问额外的 1 跳,而预先设置的地址可能总共只有 1 uop。例如,在 4x 展开循环中,您可以通过使用 1 uop 计算地址来节省 3 uop,然后使用基址 + 偏移寻址 4 次而不是索引。 (2认同)

EOF*_*EOF 50

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

我对程序集进行了一些修改:
只需使用32位版本,将所有32位指令/寄存器替换为程序内部popcount循环中的64位版本.观察:代码和32位版本一样快!

这显然是一个hack,因为变量的大小不是真正的64位,因为程序的其他部分仍然使用32位版本,但只要内部popcount-loop主导性能,这是一个好的开始.

然后我从32位版本的程序中复制了内部循环代码,将其破解为64位,使用寄存器进行调整,使其成为64位版本内部循环的替代品.此代码的运行速度与32位版本一样快.

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

(警告:我破坏了装配,可能在没有注意的情况下破坏了一些东西.我不这么认为.)

  • “此外,gcc 认为 64 位整数 [...] 更好,因为使用 uint_fast32_t 会导致 gcc 使用 64 位 uint。” 不幸的是,令我遗憾的是,这些类型背后没有魔法,也没有深入的代码自省。我还没有看到它们为整个平台上的每个可能的位置和每个程序提供了除作为单个 typedef 之外的任何其他方式。在类型的确切选择背后可能已经考虑了很多,但是它们中的每一个的一个定义不可能适合所有的应用程序。进一步阅读:/sf/ask/288140821/。 (3认同)
  • @Keno 那是因为必须定义 `sizeof(uint_fast32_t)`。如果您不允许这样做,您可以使用这种技巧,但这只能通过编译器扩展来完成。 (3认同)

Non*_*upt 24

这不是答案,但如果我将结果置于评论中,则很难理解.

我用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
Run Code Online (Sandbox Code Playgroud)

铿锵有笑 uint64_t size=1<<20;

unsigned    41950110000 0.623406 sec    16.8201 GB/s
uint64_t    41950110000 0.623685 sec    16.8126 GB/s
Run Code Online (Sandbox Code Playgroud)

我也试过:

  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位更快.

这三个因素共同与观察到的看似相互矛盾的结果相匹配.

  • 有意思,你能添加编译器版本和编译器标志吗?*最好的事情是在你的机器上,结果转过来,即使用u64更快*.到目前为止,我从未想过我的循环变量有哪种类型,但似乎我下次要三思而后:). (4认同)
  • @gexicide:我不打算从16.8201跳到16.8126让它"更快". (2认同)
  • @Mehrdad:我的意思是"12.9"和"16.8"之间的跳跃,所以`unsigned`在这里更快.在我的基准测试中,情况恰恰相反,即`unsigned`为26,`uint64_t`为15 (2认同)

Gen*_*ene 10

我无法给出权威的答案,但提供可能原因的概述.该参考文献非常清楚地表明,对于循环体中的指令,延迟和吞吐量之间存在3:1的比率.它还显示了多次发送的效果.由于在现代x86处理器中存在(给 - 取)三个整数单元,因此通常可以在每个周期发送三个指令.

因此,在峰值流水线和多个调度性能之间以及这些机制的失败之间,我们的性能因数为6.众所周知,x86指令集的复杂性使得很容易发生奇怪的破坏.上面的文档有一个很好的例子:

64位右移的Pentium 4性能非常差.64位左移以及所有32位移位都具有可接受的性能.似乎从ALU的高32位到低32位的数据路径没有很好地设计.

我个人遇到了一个奇怪的情况,在一个四核芯片的特定核心上,热循环运行得相当慢(如果我记得,AMD就是这样).实际上,通过关闭核心,我们在map-reduce计算上获得了更好的性能.

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

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

我知道这不是一个严谨的分析,但这一个似是而非的解释.

  • 不幸的是,从那以后(Core 2?),除了乘法/除法之外,32位和64位整数运算之间几乎没有性能差异 - 这在此代码中不存在. (2认同)

rcg*_*ldr 10

我尝试使用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_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;
   }
Run Code Online (Sandbox Code Playgroud)

汇编代码: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
Run Code Online (Sandbox Code Playgroud)


Dan*_*lov 9

你有没有试过去-funroll-loops -fprefetch-loop-arraysGCC?

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

[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
Run Code Online (Sandbox Code Playgroud)

  • 但是,你的结果仍然很奇怪(首先是unsigned更快,然后uint64_t更快)因为展开不能解决false依赖的主要问题. (3认同)

Ben*_*igt 7

您是否尝试过在循环外移动还原步骤?现在你有一个真正不需要的数据依赖.

尝试:

  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];
Run Code Online (Sandbox Code Playgroud)

你也有一些奇怪的别名,我不确定是否符合严格的别名规则.

  • 这是我读完这个问题后我做的第一件事.打破依赖链.事实证明,性能差异并没有改变(至少在我的电脑上 - 英特尔Haswell与GCC 4.7.3). (2认同)

小智 5

TL; DR:__builtin改为使用内在函数.

我能够gcc通过使用__builtin_popcountll相同的汇编指令生成4.8.4(甚至是gcc.godbolt.org上的4.7.3)为此生成最佳代码,但没有那个错误依赖性错误.

我不是100%肯定我的基准测试代码,但objdump输出似乎分享我的观点.我使用其他一些技巧(++ivs i++)使编译器在没有任何movl指令的情况下为我展开循环(奇怪的行为,我必须说).

结果:

Count: 20318230000  Elapsed: 0.411156 seconds   Speed: 25.503118 GB/s
Run Code Online (Sandbox Code Playgroud)

基准代码:

#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;
}
Run Code Online (Sandbox Code Playgroud)

编译选项:

gcc --std=gnu99 -mpopcnt -O3 -funroll-loops -march=native bench.c -o bench
Run Code Online (Sandbox Code Playgroud)

GCC版本:

gcc (Ubuntu 4.8.4-2ubuntu1~14.04.1) 4.8.4
Run Code Online (Sandbox Code Playgroud)

Linux内核版本:

3.19.0-58-generic
Run Code Online (Sandbox Code Playgroud)

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:
Run Code Online (Sandbox Code Playgroud)

  • 运行`-funroll-loops`恰好使得代码不会出现在由'popcnt`的false dep创建的循环携带的依赖链上的瓶颈.使用不了解错误依赖关系的旧编译器版本是一种风险.如果没有`-funroll-loops`,gcc 4.8.5的循环将会影响popcnt延迟而不是吞吐量,[因为它计入`rdx`](https://godbolt.org/g/lnRHlb).相同的代码,[由gcc 4.9.3编译](https://godbolt.org/g/GzFt6I)添加了一个`xor edx,edx`来打破依赖链. (3认同)
  • 对于旧编译器,您的代码仍然容易受到OP所经历的完全相同的性能变化的影响:看似微不足道的更改可能会使gcc变慢,因为它不知道它会导致问题.**在一个旧编译器中查找恰好在一个案例中工作的东西*不是*问题.** (3认同)
  • 记录一下,GCC上的x86intrin.h函数_mm_popcnt_ *是[__builtin_popcount *周围的强制内联包装器](https://github.com/gcc-mirror/gcc/blob/master/gcc /config/i386/popcntintrin.h); 内联应该使一个完全等同于另一个。我非常怀疑您会发现在它们之间进行切换可能会导致任何差异。 (2认同)

Sol*_*eil 5

这不是答案,而是对 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
Run Code Online (Sandbox Code Playgroud)

使用英特尔编译器 2021:

unsigned        209695540000    1.70845 sec     30.688 GB/s
uint64_t        209695540000    1.57956 sec     33.1921 GB/s
Run Code Online (Sandbox Code Playgroud)

根据 Mysticial 的回答,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; // 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;
}
Run Code Online (Sandbox Code Playgroud)

和主要的拆卸:

.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 ;