Я надеюсь, что я уменьшил свой вопрос до простого и воспроизводимого тестового примера. Источник (здесь) содержит 10 копий одинакового простого цикла. Каждый цикл имеет вид:
#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" переменной важна, поскольку она заставляет значение читать и записывать из памяти на каждой итерации. Каждый цикл согласован с 64 байтами с использованием "-falign-loops = 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>
Я запускаю Linux 3.11 на Intel Haswell i7-4470. Я компилирую программу с помощью GCC 4.8.1 и командной строки:
gcc -std=gnu99 -O3 -falign-loops=64 -Wall -Wextra same-function.c -o same-function
Я также использую атрибут ((noinline)) в источнике, чтобы сделать сборку более четкой, но это не обязательно для наблюдения за проблемой. Я нахожу самые быстрые и медленные функции с циклом оболочки:
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%. Мы подтверждаем это, неоднократно проверяя эти два случая другим способом:
[email protected]$ 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
[email protected]$ 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
Учитывая это, мы перечитываем каждое слово в каждом архитектурном пособии Intel, когда-либо написанном, просеиваем каждую страницу на всей веб-странице, в которой упоминаются слова "компьютер" или "программирование", и медитируют изолированно на вершине горы на 6 года. Если вы не добьетесь какого-либо просветления, мы дойдем до цивилизации, побримся бородой, нанесите ванну и спросим экспертов StackOverflow:
Что здесь может быть?
Изменить: С помощью Вениамина (см. его ответ ниже) я придумал еще более лаконичный тестовый пример. Это автономные 20 линий сборки. Переход от использования SUB к SBB приводит к 15% -ной разнице в производительности, хотя результат остается тем же и выполняется одинаковое количество инструкций. Объяснения? Я думаю, я приближаюсь к одному.
; Minimal example, see also http://stackoverflow.com/q/26266953/3766665
; 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