Я хочу измерить время, которое атомарный fetch_add в C ++ занимает при разных настройках. Я написал что-то вроде этого:
atomic<uint64_t> x(0);
for (uint64_t i = 0; i < REPS; i+=1g) {
x.fetch_add(1);
}
Так что если REPS
достаточно высока, я предполагаю, что сможет усреднить fetch_add
в секунду происходит. Во-первых, мне нужно было проверить, что большую часть времени действительно было потрачено в fetch_add, в отличие, например, от издержек цикла. Поэтому я запустил перф, чтобы сделать это.
Это сборка от objdump:
400ed0: b8 00 b4 c4 04 mov $0x4c4b400,%eax
400ed5: 0f 1f 00 nopl (%rax)
400ed8: f0 83 05 7c 22 20 00 lock addl $0x1,0x20227c(%rip)
400edf: 01
400ee0: 83 e8 01 sub $0x1,%eax
400ee3: 75 f3 jne 400ed8 <_Z10incrsharedv+0x8>
perf (для события циклов) говорит, что 100% циклов идут в sub $0x1,%eax
, в отличие от того, что я ожидал, lock addl $0x1,0x20227c(%rip)
или прыжок. Есть идеи почему? это точно, или это просто артефакт измерения? Во втором случае, почему бы систематически относить задержку к sub
линия, а не addl
?
TL; DR: попробуйте использовать :pp
суффикс, для некоторых событий процессор может помочь вам дать более точные данные аннотации.
Более длинная версия:
Пытаясь исследовать поведение, которое я описал, я также попытался использовать следующий более развернутый цикл. Я думаю, что это решает вопрос в некоторой степени.
for (uint64_t i = 0; i < REPS; i+=10) {
x.fetch_add(1, ORDER);
x.fetch_add(1, ORDER);
x.fetch_add(1, ORDER);
x.fetch_add(1, ORDER);
x.fetch_add(1, ORDER);
x.fetch_add(1, ORDER);
x.fetch_add(1, ORDER);
x.fetch_add(1, ORDER);
x.fetch_add(1, ORDER);
x.fetch_add(1, ORDER);
}
Когда используешь perf record -e cycles
Полученный перф аннот является:
: 0000000000400f00 <incr(std::atomic<unsigned long>&)>:
0.00 : 400f00: mov $0x3d0900,%eax
0.00 : 400f05: nopl (%rax)
0.00 : 400f08: lock addq $0x1,(%rdi)
10.93 : 400f0d: lock addq $0x1,(%rdi)
9.77 : 400f12: lock addq $0x1,(%rdi)
10.22 : 400f17: lock addq $0x1,(%rdi)
8.97 : 400f1c: lock addq $0x1,(%rdi)
10.39 : 400f21: lock addq $0x1,(%rdi)
9.87 : 400f26: lock addq $0x1,(%rdi)
10.48 : 400f2b: lock addq $0x1,(%rdi)
9.70 : 400f30: lock addq $0x1,(%rdi)
10.19 : 400f35: lock addq $0x1,(%rdi)
9.49 : 400f3a: sub $0x1,%rax
0.00 : 400f3e: jne
Когда я изменяю количество вызовов для извлечения и добавляю к 5, определяются 5 горячих точек. Этот результат говорит о том, что в приписывании циклов в этом случае есть систематическая ошибка в одной инструкции:
Perf Wiki включает в себя следующее предупреждение:
«Выборка на основе прерываний вводит заносы на современных процессорах. Это означает, что указатель инструкций, хранящийся в каждом примере, обозначает место, где программа была прервана для обработки прерывания PMU, а не место, где счетчик фактически переполняется»
«Расстояние между этими двумя точками может составлять несколько десятков инструкций или более, если были взяты ветви».
Так что, похоже, я должен считать себя удачливым, потому что аннотация отключена одним;).
Обновить: Процессоры Intel поддерживают функцию под названием PEBS (точная выборка на основе событий), которая делает корреляцию указателя инструкций со встречным событием намного менее подверженной ошибкам Смотрите это сообщение на форуме.
Вы можете получить доступ к этой функции через perf
а также для выбранных счетчиков:
С помощью perf record -e cycles:pp
вместо этого (обратите внимание на :pp
суффикс) результат аннотирования на этот раз:
: 0000000000400f00 <incr(std::atomic<unsigned long>&)>:
0.00 : 400f00: mov $0x3d0900,%eax
0.00 : 400f05: nopl (%rax)
10.75 : 400f08: lock addq $0x1,(%rdi)
10.15 : 400f0d: lock addq $0x1,(%rdi)
10.00 : 400f12: lock addq $0x1,(%rdi)
9.22 : 400f17: lock addq $0x1,(%rdi)
10.21 : 400f1c: lock addq $0x1,(%rdi)
9.75 : 400f21: lock addq $0x1,(%rdi)
9.95 : 400f26: lock addq $0x1,(%rdi)
10.02 : 400f2b: lock addq $0x1,(%rdi)
10.18 : 400f30: lock addq $0x1,(%rdi)
9.75 : 400f35: lock addq $0x1,(%rdi)
0.00 : 400f3a: sub $0x1,%rax
0.00 : 400f3e: jne 400f08
Что подтверждает догадку. Это решение может быть полезно в гораздо более сложных ситуациях с прыжками.