Replacing a 32-bit loop counter with 64-bit introduces crazy performance deviations
我一直在寻找最快的方法来访问
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | #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); } |
如您所见,我们创建一个随机数据缓冲区,其大小为
我这样编译它(G++版本:Ubuntu 4.8.2-19Ubuntu1):
1 | g++ -O3 -march=native -std=c++11 test.cpp -o test |
以下是我的haswell核心i7-4770k CPU在3.50 GHz下运行
- 无符号41959360000 0.401554秒26.113 GB/s
- uint64_t 41959360000 0.759822秒13.8003 GB/s
如您所见,
1 | clang++ -O3 -march=native -std=c++11 teest.cpp -o test |
结果:
- 无符号41959360000 0.398293秒26.3267 GB/s
- uint64_t 41959360000 0.680954秒15.3986 GB/s
所以,这几乎是相同的结果,仍然是奇怪的。但现在变得非常奇怪了。我将从输入中读取的缓冲区大小替换为常量
1 | uint64_t size = atol(argv[1]) << 20; |
到
1 | uint64_t size = 1 << 20; |
因此,编译器现在知道了编译时缓冲区的大小。也许它可以添加一些优化!以下是
- 无符号41959360000 0.509156秒20.5944 GB/s
- uint64_t 41959360000 0.508673秒20.6139 GB/s
现在,这两种版本的速度都一样快。然而,江户十一〔一〕的速度更慢了!它从
- 无符号41959360000 0.677009秒15.4884 GB/s
- uint64_t 41959360000 0.676909秒15.4906 GB/s
等等,什么?现在,两个版本都降到了15 GB/s的慢速值。因此,将非常量替换为常量值甚至会导致两种情况下的clang代码都很慢!
我让一位同事用常春藤桥CPU编译我的基准测试。他得到了相似的结果,所以似乎不是哈斯韦尔。因为两个编译器在这里产生奇怪的结果,所以它似乎也不是一个编译器bug。我们这里没有AMD的CPU,所以我们只能用Intel测试。
请再疯狂一点!以第一个例子(带有
1 | static uint64_t size=atol(argv[1])<<20; |
以下是我在g++中的结果:
- 无符号41959360000 0.396728秒26.4306 GB/s
- uint64_t 41959360000 0 0.509484秒20.5811 GB/s
是的,还有另一个选择。我们仍然有与
你能解释一下这些结果吗?特别是:
u32 和u64 之间有什么区别?- 如何将非常量替换为常量缓冲区大小,从而触发不太理想的代码?
- 插入
static 关键字如何使u64 循环更快?比我同事电脑上的原始代码还要快!
我知道优化是一个棘手的领域,但是,我从未想过如此小的更改会导致100%的执行时间差异,并且像恒定缓冲区大小这样的小因素会再次完全混合结果。当然,我总是希望有能够弹出26 GB/s的版本。我能想到的唯一可靠的方法是复制粘贴此情况下的程序集并使用内联程序集。这是我唯一能摆脱那些似乎对小改动很生气的编译器的方法。你怎么认为?是否有其他方法可以可靠地获取性能最高的代码?
拆卸这是拆卸图
罪魁祸首:错误的数据依赖(编译器甚至不知道)
在Sandy/Ivy Bridge和Haswell处理器上,说明:
1 | popcnt src, dest |
似乎对目标寄存器
这种依赖性不仅支持单循环迭代中的4
在您的例子中,速度是粘在(错误)依赖链上的东西的直接结果,这取决于寄存器分配器决定做什么。
- 13 GB/s有一个链:
popcnt -add -popcnt -popcnt &rarr;下一个迭代 - 15GB/s有一个链:
popcnt -add -popcnt -add &rarr;下一个迭代 - 20 GB/s有一个链:
popcnt -popcnt &rarr;下一个迭代 - 26 GB/s有一个链:
popcnt -popcnt &rarr;下一个迭代
20 GB/s和26 GB/s之间的差异似乎是间接寻址的一个小错误。不管怎样,一旦达到这个速度,处理器就会开始碰到其他瓶颈。
为了测试这一点,我使用了内联程序集来绕过编译器并获得我想要的程序集。我还拆分了
结果如下:
Sandy Bridge [email protected] GHz:(可在底部找到完整的测试代码)
- 一般合同条款第4.6.3款:
g++ popcnt.cpp -std=c++0x -O3 -save-temps -march=native 。 - Ubuntu 12
不同寄存器:18.6195 GB/s
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | .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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | .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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | .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都没有意识到
(更新:从4.9.2版开始,GCC意识到这种错误的依赖性,并在启用优化时生成代码来补偿它。其他供应商(包括Clang、MSVC,甚至英特尔自己的ICC)的主要编译器还没有意识到这种微体系结构的不稳定,不会发出补偿它的代码。)
为什么CPU有如此错误的依赖性?
我们只能推测,但英特尔对许多双操作数指令的处理方式可能相同。像
AMD处理器似乎没有这种错误的依赖性。
完整测试代码如下供参考:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 | #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 \t" "add %4, %0 \t" "popcnt %5, %5 \t" "add %5, %1 \t" "popcnt %6, %6 \t" "add %6, %2 \t" "popcnt %7, %7 \t" "add %7, %3 \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 \t" "add %%rax, %0 \t" "popcnt %5, %%rax \t" "add %%rax, %1 \t" "popcnt %6, %%rax \t" "add %%rax, %2 \t" "popcnt %7, %%rax \t" "add %%rax, %3 \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 \t" // <--- Break the chain. "popcnt %4, %%rax \t" "add %%rax, %0 \t" "popcnt %5, %%rax \t" "add %%rax, %1 \t" "popcnt %6, %%rax \t" "add %%rax, %2 \t" "popcnt %7, %%rax \t" "add %%rax, %3 \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这个基准改变了(错误的)依赖链中的
1 2 3 4 5 | 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 |
我编写了一个等效的C程序来进行实验,我可以证实这种奇怪的行为。另外,
这不是一个答案,但如果我把结果放在评论中,就很难阅读了。
我使用Mac Pro(Westmire 6核Xeon 3.33 GHz)获得这些结果。我用
1 2 | unsigned 41950110000 0.811198 sec 12.9263 GB/s uint64_t 41950110000 0.622884 sec 16.8342 GB/s |
与
1 2 | unsigned 41950110000 0.623406 sec 16.8201 GB/s uint64_t 41950110000 0.623685 sec 16.8126 GB/s |
我还试图:
这是我的疯狂猜测:
速度系数分为三部分:
代码缓存:
uint64_t 版本的代码大小较大,但这对我的Xeon CPU没有影响。这会使64位版本变慢。使用说明。注意,不仅循环计数,而且在两个版本中,缓冲区是通过32位和64位索引访问的。使用64位偏移量访问指针请求专用的64位寄存器和寻址,而您可以使用immediate进行32位偏移量。这可能会使32位版本更快。
指令仅在64位编译时发出(即预取)。这使得64位更快。
这三个因素加在一起与观察到的看似矛盾的结果相吻合。
我在Visual Studio 2013 Express中尝试了这个方法,使用指针而不是索引,这会加快进程。我怀疑这是因为地址是偏移+寄存器,而不是偏移+寄存器+(寄存器<<3)。C++代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | $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 |
我不能给出权威性的答案,但提供可能原因的概述。这个参考非常清楚地表明,对于循环体中的指令,延迟和吞吐量之间的比率为3:1。它还显示了多次调度的效果。由于在现代x86处理器中有三个整数单元,因此通常可以每个周期发送三个指令。
因此,在峰值管道和多个调度性能以及这些机制的故障之间,我们有一个性能系数为6。众所周知,x86指令集的复杂性使得它很容易发生奇怪的中断。上面的文档有一个很好的例子:
The Pentium 4 performance for 64-bit right shifts is really poor. 64-bit left shift as well as all 32-bit shifts have acceptable performance. It appears that the data path from the upper 32 bits to the lower 32 bit of the ALU is not well designed.
我个人遇到了一个奇怪的情况,一个热循环在一个四核芯片的特定核心上运行得相当慢(如果我记得的话,是AMD)。实际上,通过关闭核心,我们在map-reduce计算中获得了更好的性能。
这里我的猜测是对整数单元的争用:
使用静态变量引起的变化,我猜这只会导致指令的轻微重新排序,这也是32位代码处于争用临界点的另一个线索。
我知道这不是一个严格的分析,但这是一个合理的解释。
你试过把
通过这些额外的优化,我得到了以下结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | [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 |
你试过把减速步移出回路吗?现在您有一个真正不需要的数据依赖项。
尝试:
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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]; |
你还有一些奇怪的混叠,我不确定是否符合严格的混叠规则。
tl;dr:使用
我能够让
我对我的基准代码没有100%的把握,但
结果:
1 | Count: 20318230000 Elapsed: 0.411156 seconds Speed: 25.503118 GB/s |
基准代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | #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> ", 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 ", count, (double)(toc - tic) / CLOCKS_PER_SEC, ((10000.0*size)/(((double)(toc - tic)*1e+9) / CLOCKS_PER_SEC))); return 0; } |
编译选项:
1 | gcc --std=gnu99 -mpopcnt -O3 -funroll-loops -march=native bench.c -o bench |
GCC版本:
1 | gcc (Ubuntu 4.8.4-2ubuntu1~14.04.1) 4.8.4 |
Linux内核版本:
1 | 3.19.0-58-generic |
CPU信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | 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: |
好的,我想对OP提出的一个子问题提供一个小的答案,这个问题在现有问题中似乎没有解决。注意,我没有做过任何测试、代码生成或反汇编,只是想和大家分享一个想法,以便大家可以加以阐述。
为什么问题行:
我将查看为访问
由于变量只有一个副本,不管它是否声明为
好的,从显而易见的开始,记住函数的所有局部变量(连同参数)都在堆栈上提供了空间,用作存储。现在,显然,main()的堆栈帧永远不会清除,只生成一次。好吧,那我们把它变成1号[0号]怎么样?在这种情况下,编译器知道在进程的全局数据空间中保留空间,这样就不能通过删除堆栈帧来清除位置。但是,我们只有一个地点,那有什么区别呢?我怀疑这与堆栈上内存位置的引用方式有关。
当编译器生成符号表时,它只为标签和相关属性(如大小等)创建一个条目。它知道必须在内存中保留适当的空间,但在执行活动性分析和可能的寄存器分配之后,在进程中稍晚些时候才实际选择该位置。那么,链接器如何知道为最终程序集代码向机器代码提供什么地址?它要么知道最终位置,要么知道如何到达该位置。对于堆栈,很容易引用基于位置的一个两个元素,即指向stack frame的指针,然后指向该帧的偏移量。这基本上是因为链接器在运行时之前不知道stackframe的位置。
首先,尝试评估峰值性能-查看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,…)。倒计时8)并在最后进行总结。或者甚至创建一个计数数组[8]并累积到它的元素中——也许,它甚至会被矢量化,您将获得更好的吞吐量。好的。
另一方面,不要让基准跑一秒钟,先热身核心,然后循环跑至少10秒或100秒以上。否则,您将在硬件中测试电源管理固件和DVFS实现:)好的。
P.P.S.我听到了关于基准测试到底要运行多长时间的无休止的争论。最聪明的人甚至会问为什么10秒不是11秒或12秒。我应该承认这在理论上很有趣。在实践中,您只需连续运行基准100次并记录偏差。真有趣。大多数人都会更改源代码,然后运行工作台一次,以获取新的性能记录。做正确的事。好的。
还是不相信?只需使用上面的c-version of benchmark by assp1r1n3(https://stackoverflow.com/a/37026212/9706746)并在retry循环中尝试100而不是10000。好的。
我的7960x显示,重试=100:好的。
计数:203182300经过:0.008385秒速度:12.505379 GB/s好的。
计数:203182300经过:0.011063秒速度:9.478225 GB/s好的。
计数:203182300经过:0.011188秒速度:9.372327 GB/s好的。
计数:203182300经过:0.010393秒速度:10.089252 GB/s好的。
计数:203182300经过:0.009076秒速度:11.553283 GB/s好的。
重试=10000:好的。
计数:20318230000经过:0.661791秒速度:15.844519 GB/s好的。
计数:20318230000经过:0.665422秒速度:15.758060 GB/s好的。
计数:20318230000经过:0.660983秒速度:15.863888 GB/s好的。
计数:20318230000经过:0.665337秒速度:15.760073 GB/s好的。
计数:20318230000经过:0.662138秒速度:15.836215 GB/s好的。
P.P.P.S.最后,关于"接受的回答"和其他迷雾;—)好的。
让我们用assp1r1n3的答案-他有2.5GHz的内核。popcnt有1个时钟throughput,他的代码使用64位popcnt。因此,对于他的设置,数学是2.5GHz*1时钟*8字节=20 GB/s。他看到25GB/s,可能是由于涡轮增压到3GHz左右。好的。
因此,请访问ark.intel.com并查找i7-4870HQ:https://ark.intel.com/products/83504/intel-core-i7-4870hq-processor-6m-cache-up-to-3-70-ghz-?Q= I7—48 70HQ好的。
该内核最高可运行3.7GHz,其硬件的实际最大速率为29.6GB/s。那么,另一个4Gb/s在哪里呢?也许,它花费在循环逻辑和每个迭代中的其他周围代码上。好的。
现在,这种错误的依赖在哪里?硬件几乎以峰值速率运行。也许我的数学不好,有时会发生。)好的。
P.P.P.P.P.S.仍然有人认为HW勘误表是罪魁祸首,所以我遵循建议并创建了内联ASM示例,请参见下面的内容。好的。
在我的7960x上,第一个版本(对cnt0的单输出)以11MB/s的速度运行,第二个版本(输出到cnt0、cnt1、cnt2和cnt3)以33MB/s的速度运行。有人会说-哇!这是输出依赖关系。好的。
好吧,也许,我的观点是这样写代码没有意义,这不是输出依赖性问题,而是愚蠢的代码生成。我们不是在测试硬件,而是在编写代码来释放最大的性能。你可以期待hw ooo应该重命名并隐藏那些"输出依赖项",但是,gash,只要做正确的事情,你就永远不会面临任何神秘。好的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | 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: \t" "popcnt %2, %1 \t" "popcnt %2, %1 \t" "popcnt %2, %1 \t" "popcnt %2, %1 \t" "subq $4, %0 \t" "jnz 1b \t" :"+q" (len),"=q" (cnt0) :"q" (val) : ); #else __asm__ __volatile__ ( "1: \t" "popcnt %5, %1 \t" "popcnt %5, %2 \t" "popcnt %5, %3 \t" "popcnt %5, %4 \t" "subq $4, %0 \t" "jnz 1b \t" :"+q" (len),"=q" (cnt0),"=q" (cnt1),"=q" (cnt2),"=q" (cnt3) :"q" (val) : ); #endif return cnt0; } |
好啊。