用64位替换32位的循环计数器,会导致在Intel CPU上使用_mm_popcnt_u64时出现性能差异。

1627

我正在寻找快速计算大型数据数组的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兆字节。之后,我们遍历缓冲区并使用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

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

  • unsigned 41959360000 0.401554秒 26.113 GB/s
  • uint64_t 41959360000 0.759822秒 13.8003 GB/s

正如您所看到的,uint64_t版本的吞吐量仅为unsigned版本的一半!问题似乎是生成了不同的汇编代码,但为什么呢?起初,我认为这是编译器的错误,因此我尝试了clang ++(Ubuntu Clang 版本3.4-1ubuntu3):

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

结果: 测试1

  • 无符号 41959360000 0.398293 秒 26.3267 GB/s
  • uint64_t 41959360000 0.680954 秒 15.3986 GB/s

所以,结果几乎相同,仍然很奇怪。 但现在变得非常奇怪。 我用一个常量1替换了从输入读取的缓冲区大小,因此我更改为:

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

uint64_t size = 1 << 20;

因此,编译器现在在编译时就知道缓冲区大小。也许它可以添加一些优化!以下是 g++ 的数字:
- unsigned 41959360000 0.509156 秒 20.5944 GB/s - uint64_t 41959360000 0.508673 秒 20.6139 GB/s
现在,两个版本的速度都一样快。然而,unsigned 变得更慢了!它从 26 GB/s 降至 20 GB/s,因此将一个非常量替换为常量值导致了一次非最优化。说实话,我不知道这里发生了什么!但是现在来看看带有新版本的 clang++
- unsigned 41959360000 0.677009 秒 15.4884 GB/s - uint64_t 41959360000 0.676909 秒 15.4906 GB/s

等等,什么?现在,两个版本的速度都降到了缓慢的15GB/s。因此,即使是将非常量替换为常量值,对于Clang来说,在两种情况下代码也会变得更慢!

我请一位拥有Ivy Bridge CPU的同事编译我的基准测试。他得到了类似的结果,所以这似乎不是Haswell的问题。由于两个编译器在这里产生了奇怪的结果,所以它也不像是编译器的错误。我们这里没有AMD CPU,所以我们只能使用Intel进行测试。

更多疯狂,请!

取第一个例子(带有atol(argv[1]))并在变量前加上static,即:

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

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

  • unsigned 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

我的问题

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

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

我知道优化是一个棘手的领域,然而,我从未想过这样的微小改变会导致执行时间的100%差异,而像常量缓冲区大小这样的小因素又可以完全混淆结果。当然,我总是希望拥有能够弹出26 GB/s的版本。我能想到的唯一可靠的方法是复制并粘贴此情况下的汇编,并使用内联汇编。这是我摆脱似乎对微小更改疯狂的编译器的唯一方法。你认为呢?是否还有其他可靠地获取最佳性能代码的方法?

反汇编

以下是各种结果的反汇编:

g++ / u32 / 非常量缓冲区大小的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

来自 g++ / u64 / 非常量缓冲区 的 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

来自clang++ / u64 / 非常量缓冲区的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,提供每秒15GB的版本:

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关键字时所看到的那样!将来,当编写对系统性能至关重要的紧凑且热的循环时,我将始终在各种编译器上测试各种替代方案。
有趣的是,即使我已经将循环展开了四次,性能差异仍然如此之大。因此,即使你展开循环,仍然可能受到主要性能偏差的影响。非常有趣。

19
这里有很多评论!您可以在聊天记录中查看它们,如果您愿意,甚至可以在那里留下您自己的评论,但请不要在此处添加任何内容! - Shog9
5
请参见 GCC 问题62011,popcnt 指令中的虚假数据依赖。有人提供了它,但似乎在清理过程中丢失了。 - jww
我无法确定,但是这是否是具有静态版本的反汇编之一?如果不是,您可以编辑帖子并添加吗? - Kelly S. French
11个回答

1741

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

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

popcnt  src, dest

看起来这个指令与目标寄存器dest存在虚假依赖。即使该指令只对其进行写操作,该指令将等待dest准备就绪后再执行。这种虚假依赖现在被英特尔记录为勘误HSD146(Haswell)SKL029(Skylake)

Skylake修复了lzcnttzcnt的问题
Cannon Lake(和Ice Lake)修复了popcnt的问题。
bsf/bsr存在真实的输出依赖:当输入为0时,输出不会改变。(但无法通过内部函数利用此功能 - 只有AMD记录了它,而编译器没有暴露它。)

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


This dependency不仅仅支撑了单个循环迭代中的4个popcnt,它还可以跨越循环迭代,使得处理器无法并行化不同的循环迭代。 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并不是最常用的指令。因此,一个主要的编译器错过这样的东西并不令人惊讶。似乎也没有任何文档提到这个问题。如果英特尔不披露它,那么除非有人偶然遇到它,否则外界将不会知道。

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

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

我们可以推测:它在与bsf/bsr相同的执行单元上运行,这两个指令确实有输出依赖关系。 (POPCNT在硬件上是如何实现的?) 对于这些指令,英特尔将输入为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数量。

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

5
大家好!这里有很多以前的评论;在留下新评论之前,请查看存档,谢谢。 - Shog9
1
@JustinL 看起来这个问题在 Clang 7.0 中已经解决了。 - Dan M.
1
@Noah 复杂的寻址也会导致非层压,这可能解释了差异,或者只是一个对齐调整,它总是会影响事情。 - BeeOnRope
3
@Noah,我没有看汇编代码,只看了这些评论,但似乎所有版本都使用了索引寻址?可能我将“间接”误读为“索引”。我不太确定OP所说的“间接寻址”是什么意思。不过,回答你的问题,去膜化的一个普遍方式可能导致每次访问多1跳,而预先设置地址可能只需要1个uop总数。例如,在4倍展开循环中,通过使用1个uop计算地址,并且然后使用基址+偏移量寻址4次,而不是使用索引寻址,您可能会节省3个uop。 - BeeOnRope
1
是的,我指的是在中间重命名时保存的 uop,这是一个重要的瓶颈,因为它是最窄的瓶颈(也就是说,这就是英特尔芯片为什么“4宽”的原因)。如果我没表达清楚,对不起,我的意思并不是它可以在执行时以某种方式避免负载操作本身(始终需要 p23 uop,问题只是融合时间有多长)。@Noah - BeeOnRope
显示剩余16条评论

62

我编写了一个等效的 C 程序进行实验,并确认出现了这种奇怪的行为。更重要的是,gcc 认为使用 64 位整数(本来应该是 size_t...)更好,因为使用 uint_fast32_t 导致 gcc 使用了 64 位无符号整数。

我通过汇编代码做了一些测试:
只需将程序内部 popcount 循环中的所有 32 位指令/寄存器替换为 64 位版本即可。观察:代码与 32 位版本一样快!

显然,这是一个 hack,因为变量的大小并不是真正的 64 位,因为程序的其他部分仍在使用 32 位版本,但只要内部 popcount 循环占主导地位,这就是一个良好的开端。

然后,我从程序的 32 位版本中复制了内部循环代码,将其改为 64 位,并操纵寄存器,使其成为 64 位版本内部循环的替代品。这段代码也像 32 位版本一样快。

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

(警告:我修改了汇编代码,可能没有注意到某些问题。但我认为不会出现这种情况。)


7
此外,gcc 认为使用 64 位整数[…] 更好,因为使用 uint_fast32_t 会导致 gcc 使用 64 位无符号整数。遗憾的是,我很遗憾地告诉你,这些类型背后没有魔法或深入的代码内省。我还没有看到它们以除每个可能的位置和整个平台上的每个程序之外的任何其他方式提供。这些类型的确切选择很可能经过了一些思考,但是每个类型的单一定义不可能适用于将来所有可能的应用程序。更多阅读资料: https://dev59.com/zG855IYBdhLWcg3w1oHa。 - Keno
7
这是因为必须定义sizeof(uint_fast32_t)。如果不定义,可以使用编译器扩展来实现这种技巧。 - wizzwizz4

35

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

我使用一台Mac Pro电脑(Westmere 6核Xeon 3.33 GHz)得到了这些结果。我使用clang -O3 -msse4 -lstdc++ a.cpp -o a编译它(-O2得到相同的结果)。

带有uint64_t size=atol(argv[1])<<20;的clang

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

使用 uint64_t size=1<<20; 的 clang

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

我还尝试了以下操作:

  1. 改变测试顺序,结果相同,因此排除了缓存因素。
  2. for 语句反向: for (uint64_t i=size/8;i>0;i-=4)。这给出了相同的结果,并证明编译器足够聪明,不会在每次迭代中都将 size 除以 8(预期行为)。

这是我的猜测:

速度因素有三个部分:

  • 代码缓存: uint64_t 版本具有更大的代码大小,但这对我的 Xeon CPU 没有影响。这使得 64 位版本更慢。

  • 所使用的指令。请注意,不仅要循环计数,而且缓冲区在两个版本中使用 32 位和 64 位索引进行访问。使用 64 位偏移量访问指针需要专用的 64 位寄存器和寻址方式,而您可以使用立即数进行 32 位偏移量。这可能使得 32 位版本更快。

  • 只有在 64 位编译时才会发出指令(即 prefetch)。这使得 64 位更快。

这三个因素共同匹配了观察到的看似矛盾的结果。


4
有趣,你能添加编译器版本和编译器标志吗?最好的是在你的机器上,结果被反转了,也就是使用u64更快。到目前为止,我从未考虑过我的循环变量具有哪种类型,但似乎下一次我必须再想一想:)。 - gexicide
2
@gexicide:我不会说从16.8201跳到16.8126就算是“更快”了。 - user541686
2
@MehrdadпјҡжҲ‘жҢҮзҡ„и·іи·ғжҳҜеңЁ12.9е’Ң16.8д№Ӣй—ҙпјҢжүҖд»ҘеңЁиҝҷйҮҢдҪҝз”Ёunsignedжӣҙеҝ«гҖӮеңЁжҲ‘зҡ„еҹәеҮҶжөӢиҜ•дёӯпјҢжғ…еҶөжҒ°еҘҪзӣёеҸҚпјҢеҚіunsignedдёә26пјҢuint64_tдёә15гҖӮ - gexicide
@gexicide 你有注意到使用 buffer[i] 的寻址方式的不同吗? - Non-maskable Interrupt
32位偏移量可以在带立即数的指令中容纳,而64位偏移量需要一个完整的64位寄存器来计算/保存地址,然后进行解引用。 - Non-maskable Interrupt
显示剩余2条评论

14

我尝试使用指针而不是索引来实现这个,使用的是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;
   }

汇编代码: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

12
我不能给出权威答案,但提供一个可能原因的概述。这个参考文献 显示,在你的循环体内的指令中,延迟和吞吐量存在3:1的比例关系。它还显示了多个分派的影响。由于现代x86处理器中大约有(加减)三个整数单元,通常每个周期可以分派三个指令。
因此,在最高水平管道和多重分派性能以及这些机制失败之间,我们在性能上会有6个因素。众所周知,x86指令集的复杂性使得出现怪异故障相当容易。上面的文档有一个很好的例子:
“ Pentium 4对于64位右移的性能非常差。 64位左移以及所有32位移位都具有可接受的性能。看来将数据路径从ALU的高32位到低32位设计不良。”
我个人遇到过一种奇怪的情况,其中一个四核芯片(如果我记得正确)的特定核心上运行的热循环明显较慢。实际上,通过关闭该核心,我们在map-reduce计算上获得了更好的性能。
在这里,我猜测争用整数单元:即`popcnt`、循环计数器和地址计算都只能以32位宽的计数器的全速运行,但64位计数器会引起争用和管道停顿。由于总共只有大约12个周期,可能每个循环体执行4个多重分派周期,单个停顿可以合理地将运行时间影响增加一倍。
使用静态变量所引发的更改(我猜只是导致指令的轻微重新排序)是另一个暗示,表明32位代码处于某种争用的临界点。
我知道这不是严格的分析,但它是一个合理的解释。

2
不幸的是,自从(Core 2?)以来,32位和64位整数操作之间几乎没有性能差异,除了乘法/除法-这些在此代码中不存在。 - Mysticial
@Gene:请注意,所有版本都将大小存储在寄存器中,并且从未在循环中从堆栈中读取它。因此,地址计算不能混合在其中,至少不在循环内部。 - gexicide
@Gene:确实是有趣的解释!但它并没有解释主要的 WTF 点:64 位比 32 位慢是一回事,但如果是这样,64 位版本不应该可靠地比 32 位版本慢吗?相反,即使在使用编译时常量缓冲区大小时,三个不同的编译器也会生成慢速代码;将缓冲区大小更改为静态大小会完全改变事情。甚至在我的同事机器上(以及 Calvin 的答案中)有一个情况,64 位版本明显更快!它似乎是绝对不可预测的... - gexicide
@Mysticial 这就是我的观点。当IU、总线时间等没有争用时,峰值性能没有差异。参考资料清楚地表明了这一点。争用会使一切变得不同。以下是英特尔核心文献中的一个例子:“设计中包含的一项新技术是Macro-Ops Fusion,它将两个x86指令合并为一个微操作。例如,一个常见的代码序列,如比较后跟条件跳转,将成为一个微操作。不幸的是,这项技术在64位模式下无法工作。”因此,我们在执行速度上有2:1的比率差异。 - Gene
@gexicide 我明白你的意思,但你推断的比我想表达的更多。我的意思是运行最快的代码保持了管道和调度队列的满载状态。这种情况很脆弱。像将32位添加到总数据流和指令重新排序这样的微小变化就足以破坏它。简而言之,OP断言调整和测试是前进的唯一途径是正确的。 - Gene

11

你是否尝试过在GCC中添加 -funroll-loops -fprefetch-loop-arrays 参数?

使用这些额外的优化参数,我得到了以下结果:

[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

3
不过,你的结果仍然非常奇怪(首先是无符号整型更快,然后是 uint64_t 更快),因为展开循环并不能解决虚假依赖的主要问题。 - gexicide

10

你尝试过将减少步骤移到循环外吗?现在存在一个实际上并不需要的数据依赖。

尝试一下:

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

您还有一些奇怪的别名问题,我不确定是否符合严格别名规则。


2
这是我读完问题后做的第一件事。打破依赖链。结果表明,在我的电脑上(Intel Haswell with GCC 4.7.3),性能差异并没有改变。 - Nils Pipenbrinck
1
@BenVoigt:它符合严格别名。 void *char *是两种可能被别名的类型,因为它们基本上被认为是“指向某个内存块的指针”!您关于数据依赖性消除的想法对于优化很好,但它并没有回答问题。而且,正如@NilsPipenbrinck所说,它似乎没有改变任何东西。 - gexicide
1
@BenVoigt:那么你就永远无法安全地使用malloc分配任何数组,因为malloc返回void*,而你将其解释为T[]。我相信void*char*在严格别名方面具有相同的语义。但是,我想这在这里是相当离题的 :) - gexicide
1
我个人认为正确的方式是 uint64_t* buffer = new uint64_t[size/8]; /* 类型明确为 uint64_t[] */ char* charbuffer=reinterpret_cast<char*>(buffer); /* 将 uint64_t[] 与 char* 进行别名处理是安全的 */ - Ben Voigt
编译器很可能会看到四个累加器是在后面添加的,然后将它们全部卷入循环中的一个累加器中。只使用常量索引(没有&)的标量局部数组通常被视为单独的局部标量。 - greggo
显示剩余5条评论

9
TL;DR: 使用__builtin内建函数代替,可能会有所帮助。
我成功地使用__builtin_popcountll让gcc 4.8.4(甚至是gcc.godbolt.org上的4.7.3)为此生成最优代码。它使用了相同的汇编指令,但幸运地避免了由于假依赖 bug 导致的意外长循环依赖问题。
我不能百分之百确定我的基准测试代码,但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:

3
“-funroll-loops” 命令偶然能够生成不会因“popcnt”的假依赖链而形成瓶颈的代码,这只是好运。使用旧版本编译器来编译可能存在风险,因为它们不知道假依赖。如果没有使用“-funroll-loops”,gcc 4.8.5 的循环将因“popcnt”延迟而形成瓶颈,而非吞吐量,因为它在 rdx 中进行计数。相同的代码,在 gcc 4.9.3 编译 时添加了 xor edx,edx 来打破依赖关系链。 - Peter Cordes
3
在使用老编译器的情况下,你的代码仍然会容易受到与原帖作者经历相同的性能变化影响:看似微不足道的更改可能会使gcc变得缓慢,因为它不知道这会造成问题。在旧编译器上找到偶然有效的代码并不是问题的关键。请注意,我的翻译尽力保持了原文意思,同时让内容更加通俗易懂。 - Peter Cordes
3
记录一下,GCC中的x86intrin.h中的_mm_popcnt_*函数是对__builtin_popcount*的强制内联包装器;内联应该使它们完全等效。我非常怀疑你在它们之间切换时会看到任何可能引起差异的变化。 - ShadowRanger

9
这不是答案,而是针对2021年几种编译器的反馈。 测试设备为Intel CoffeeLake 9900k。
使用微软编译器(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的回答,英特尔编译器知道False Data Dependency,但微软编译器不知道。对于英特尔编译器,我使用了/QxHost(优化CPU架构为主机架构),/Oi(启用内部函数)和#include 而不是#include 。完整的编译命令如下:/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;
}

主函数的反汇编:

.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规格更新:“POPCNT指令的执行时间可能会比预计时间长。”


无论如何,ICC小心地执行popcnt same,same以避免错误依赖,但看起来它正在打败这个实际基准测试,并且不会在每个重复计数中运行popcnt,只有1/10000的数量。 - Peter Cordes
这与Godbolt上的ICC 21.1.9 -O3 -march=skylake使用向量的方式显著不同。甚至只是使用默认优化级别的-march=skylake选项。(-march=skylake或多或少相当于在您的Coffee Lake CPU上使用-QxHost)但无论如何,看起来您编译的方式实际上是测试ICC的popcnt吞吐量,而不是打败基准测试。(与clang不同,它不会自动矢量化为AVX2,而是完全使用标量popcnt) - Peter Cordes
@PeterCordes 我使用了O3,并且对于感兴趣的内在部分得到了相同的汇编代码。差异在于寄存器/堆栈使用的层面,但它们似乎是等效的。 - Soleil
1
@gexicide 针对咖啡湖: "POPCNT指令的执行时间可能比预期更长" https://www.intel.com/content/dam/www/public/us/en/documents/specification-updates/8th-gen-core-spec-update.pdf - Soleil
1
@gexicide:Skylake已经修复了lzcnt/tzcnt的错误依赖关系。直到CannonLake/IceLake才修复了popcnt的错误依赖关系。为什么打破LZCNT的“输出依赖”很重要?两者都有涉及。它们之间有关联,因为它们都在同一个执行单元上运行 - Peter Cordes
显示剩余4条评论

-3
首先,尝试估计峰值性能-查看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]数组并将其累加到其元素中-也许,它甚至会被向量化,从而获得更好的吞吐量。

P.S.永远不要在第一次运行基准测试时仅运行一秒钟,先热启动核心,然后运行循环至少10秒钟或更好的100秒钟。否则,您将测试硬件上的电源管理固件和DVFS实现 :)

P.P.S.我听说关于基准测试应该运行多长时间的无休止的辩论。大多数聪明的人甚至会问为什么是10秒而不是11或12。理论上这很有趣,但实际上,您只需连续运行一百次基准测试并记录偏差即可。这真的很有趣。大多数人确实会更改源代码并运行bench以捕获新的性能记录。做正确的事情就要做到最好。

还不确定吗?只需使用由assp1r1n3编写的上述C语言基准测试版本(https://dev59.com/E-o6XIcBkEYKwwoYIAsf#37026212),并在重试循环中尝试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个时钟吞吐量,他的代码正在使用64位popcnt。 因此,对于他的设置,数学是2.5Ghz * 1个时钟 * 8字节= 20 GB/s。 他看到了25Gb/s,可能是由于Turbo Boost达到了约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. 仍然有人建议HW勘误是罪魁祸首,所以我遵循建议并创建了内联asm示例,请参见下文。

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

好的,也许我所说的重点是不应该编写这样的代码,而且这并不是输出依赖性问题,而是愚蠢的代码生成。我们不是在测试硬件,而是在编写释放最大性能的代码。你可以期望硬件 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
如果你在核心时钟周期(而不是秒)计时,1秒对于一个微小的CPU绑定循环来说已经足够了。即使100毫秒也足以找到主要差异或检查uop计数的性能计数器。特别是在Skylake上,硬件P状态管理允许它在负载开始后的微秒内加速到最大时钟速度。 - Peter Cordes
1
clang可以使用AVX2的vpshufb自动向量化__builtin_popcountl,而且在C源代码中不需要多个累加器来实现。我不确定_mm_popcnt_u64是否只能与AVX512-VPOPCNT自动向量化。(参见使用AVX-512或AVX-2在大数据上计算1位(种群计数)) - Peter Cordes
正如我在assp1r1n3的回答中所评论的那样,它没有被虚假依赖所困扰只是运气好而已。使用他们所选的选项进行代码生成恰好避免了这个问题。这就是为什么我对那个答案投了反对票。 - Peter Cordes
2
你在开玩笑吗?我不需要“相信”那些我可以通过性能计数器在手写汇编循环中进行实验测量的事情。它们只是事实。我已经测试过,Skylake修复了lzcnt / tzcnt的错误依赖,但没有修复popcnt。请参见Intel的勘误SKL029,网址为https://www.intel.com/content/dam/www/public/us/en/documents/specification-updates/desktop-6th-gen-core-family-spec-update.pdf。此外,https://gcc.gnu.org/bugzilla/show_bug.cgi?id=62011已经“解决固定”,而不是“无效”。你声称硬件中没有输出依赖关系是没有根据的。 - Peter Cordes
1
如果你创建一个简单的循环,例如 popcnt eax, edx / dec ecx / jnz,你会期望它以每个时钟周期1次的速度运行,瓶颈在于popcnt吞吐量和分支预测吞吐量。但实际上,由于重复覆盖EAX而受到popcnt延迟的瓶颈,它只能以每3个时钟周期1次的速度运行,即使你期望它是只写操作。你有一台Skylake处理器,所以你可以自己尝试一下。 - Peter Cordes
显示剩余16条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接