我希望您能够将我的问题简化为一个简单且可重现的测试用例。源代码(可以在这里找到)包含了10个相同的简单循环。每个循环的形式如下:
变量的“volatile”很重要,因为它强制在每次迭代中从内存读取和写入值。每个循环都使用“-falign-loops=64”对齐到64字节,并且产生相同的汇编代码,除了相对于全局变量的偏移量:
它产生的结果在运行之间保持一致,误差约为1%。最快和最慢函数的确切数字因二进制布局而异。
在这种情况下,我们可以看到loop2()是执行最慢的之一,而loop6()是执行最快的之一,两者之间的差异略大于10%。我们通过使用另一种方法重复测试这两种情况来重新确认这一点:
考虑到这一点,我们重新阅读了每本英特尔体系结构手册中的每个字,筛选了整个网络中提到“计算机”或“编程”的每一页,并在山顶孤独地冥想了6年。未能获得任何启示后,我们下山回归文明,剃掉胡须,洗个澡,向StackOverflow上的专家寻求帮助:这里可能发生了什么?
编辑:在Benjamin的帮助下(见他下面的答案),我已经想出了一个更加简洁的测试案例succinct test case。它是一个独立的20行汇编程序。将SUB改为SBB会导致性能差异达到15%,尽管结果保持不变,执行的指令数也相同。解释?我认为我离答案越来越近了。
#define COUNT (1000 * 1000 * 1000)
volatile uint64_t counter = 0;
void loopN(void) {
for (int j = COUNT; j != 0; j--) {
uint64_t val = counter;
val = val + 1;
counter = val;
}
return;
}
变量的“volatile”很重要,因为它强制在每次迭代中从内存读取和写入值。每个循环都使用“-falign-loops=64”对齐到64字节,并且产生相同的汇编代码,除了相对于全局变量的偏移量:
400880: 48 8b 15 c1 07 20 00 mov 0x2007c1(%rip),%rdx # 601048 <counter>
400887: 48 83 c2 01 add $0x1,%rdx
40088b: 83 e8 01 sub $0x1,%eax
40088e: 48 89 15 b3 07 20 00 mov %rdx,0x2007b3(%rip) # 601048 <counter>
400895: 75 e9 jne 400880 <loop8+0x20>
我正在一台搭载Intel Haswell i7-4470处理器的电脑上运行Linux 3.11操作系统。使用GCC 4.8.1编译程序,命令行如下:
gcc -std=gnu99 -O3 -falign-loops=64 -Wall -Wextra same-function.c -o same-function
我还在源代码中使用属性((noinline))来使汇编更清晰,但这并不是观察问题所必需的。我使用shell循环找到最快和最慢的函数:
for n in 0 1 2 3 4 5 6 7 8 9;
do echo same-function ${n}:;
/usr/bin/time -f "%e seconds" same-function ${n};
/usr/bin/time -f "%e seconds" same-function ${n};
/usr/bin/time -f "%e seconds" same-function ${n};
done
它产生的结果在运行之间保持一致,误差约为1%。最快和最慢函数的确切数字因二进制布局而异。
same-function 0:
2.08 seconds
2.04 seconds
2.06 seconds
same-function 1:
2.12 seconds
2.12 seconds
2.12 seconds
same-function 2:
2.10 seconds
2.14 seconds
2.11 seconds
same-function 3:
2.04 seconds
2.04 seconds
2.05 seconds
same-function 4:
2.05 seconds
2.00 seconds
2.03 seconds
same-function 5:
2.07 seconds
2.07 seconds
1.98 seconds
same-function 6:
1.83 seconds
1.83 seconds
1.83 seconds
same-function 7:
1.95 seconds
1.98 seconds
1.95 seconds
same-function 8:
1.86 seconds
1.88 seconds
1.86 seconds
same-function 9:
2.04 seconds
2.04 seconds
2.02 seconds
在这种情况下,我们可以看到loop2()是执行最慢的之一,而loop6()是执行最快的之一,两者之间的差异略大于10%。我们通过使用另一种方法重复测试这两种情况来重新确认这一点:
nate@haswell$ N=2; for i in {1..10}; do perf stat same-function $N 2>&1 | grep GHz; done
7,180,104,866 cycles # 3.391 GHz
7,169,930,711 cycles # 3.391 GHz
7,150,190,394 cycles # 3.391 GHz
7,188,959,096 cycles # 3.391 GHz
7,177,272,608 cycles # 3.391 GHz
7,093,246,955 cycles # 3.391 GHz
7,210,636,865 cycles # 3.391 GHz
7,239,838,211 cycles # 3.391 GHz
7,172,716,779 cycles # 3.391 GHz
7,223,252,964 cycles # 3.391 GHz
nate@haswell$ N=6; for i in {1..10}; do perf stat same-function $N 2>&1 | grep GHz; done
6,234,770,361 cycles # 3.391 GHz
6,199,096,296 cycles # 3.391 GHz
6,213,348,126 cycles # 3.391 GHz
6,217,971,263 cycles # 3.391 GHz
6,224,779,686 cycles # 3.391 GHz
6,194,117,897 cycles # 3.391 GHz
6,225,259,274 cycles # 3.391 GHz
6,244,391,509 cycles # 3.391 GHz
6,189,972,381 cycles # 3.391 GHz
6,205,556,306 cycles # 3.391 GHz
考虑到这一点,我们重新阅读了每本英特尔体系结构手册中的每个字,筛选了整个网络中提到“计算机”或“编程”的每一页,并在山顶孤独地冥想了6年。未能获得任何启示后,我们下山回归文明,剃掉胡须,洗个澡,向StackOverflow上的专家寻求帮助:这里可能发生了什么?
编辑:在Benjamin的帮助下(见他下面的答案),我已经想出了一个更加简洁的测试案例succinct test case。它是一个独立的20行汇编程序。将SUB改为SBB会导致性能差异达到15%,尽管结果保持不变,执行的指令数也相同。解释?我认为我离答案越来越近了。
; Minimal example, see also https://dev59.com/hF8d5IYBdhLWcg3w8F--
; To build (Linux):
; nasm -felf64 func.asm
; ld func.o
; Then run:
; perf stat -r10 ./a.out
; On Haswell and Sandy Bridge, observed runtime varies
; ~15% depending on whether sub or sbb is used in the loop
section .text
global _start
_start:
push qword 0h ; put counter variable on stack
jmp loop ; jump to function
align 64 ; function alignment.
loop:
mov rcx, 1000000000
align 64 ; loop alignment.
l:
mov rax, [rsp]
add rax, 1h
mov [rsp], rax
; sbb rcx, 1h ; which is faster: sbb or sub?
sub rcx, 1h ; switch, time it, and find out
jne l ; (rot13 spoiler: foo vf snfgre ol 15%)
fin: ; If that was too easy, explain why.
mov eax, 60
xor edi, edi ; End of program. Exit with code 0
syscall
loop6
和loop1
的地址对齐方式是什么?它们都是32字节对齐的吗?它们具有相同的整体对齐方式吗? - Paul R