blog · git · desktop · images · contact
perf stat
und Hyper-Threading2021-08-19
Im letzten Blogpost ging es um „Hintergrundlast“ und ihren Einfluss auf die Ausführungszeit von anderen Prozessen (im Wesentlichen Reduzierung der CPU-Frequenz, um Überhitzung zu vermeiden). Mein Fazit war dann, dass die Messung der CPU-Zyklen ein besserer Weg ist.
Das ist zwar besser, aber immer noch sehr problematisch, insbesondere wenn man es auf modernen CPUs mit Standardkonfiguration einsetzt.
Weil ich ein bisschen paranoid bin, ist Hyper-Threading bei mir üblicherweise deaktiviert. Wahrscheinlich ist das nicht wirklich notwendig in meinen Szenarien, aber so sehr tut es auch nicht weh. Meine Kisten, obgleich von 2013/2014, sind eh mehr als schnell genug für mich. Eine „Standardkonfiguration“ ist das dann also nicht. „Standardkonfiguration“ wäre, Hyper-Threading angeschaltet zu haben.
Weil das bei mir aus war, blieb mir der folgende Effekt zunächst verborgen.
Erstmal ein kleines Programm für Linux/amd64:
.global _start
_start:
movq $1000000, %rax
.L0:
decq %rax
cmp $0, %rax
jne .L0
/* _exit(0) */
movq $60, %rax
movq $0, %rdi
syscall
Übersetzen mit:
$ as -o prog.o prog.s
$ ld -o prog prog.o
Das tut nichts Nützliches. Es wird nur eine Million Mal der Wert eines Registers um eins verringert. Der Sinn ist nur, ein Programm zu haben, von dem man ganz gut weiß, wie viele Instruktionen es ausführt:
.L0
.In Summe müssten wir also 1 + 3 * 1000000 + 3
sehen. Mal schauen:
$ perf stat -e instructions:u ./prog
Performance counter stats for './prog':
3,000,005 instructions:u
Na sowas, eine zu viel. Naja, egal, gut genug für dieses Beispiel. :)
Zur Erinnerung, unterschiedliche Instruktionen können unterschiedlich viel „kosten“, weshalb es nicht ausreicht, einfach nur diese Zahl in der Messung zu erfassen. Deswegen hat der vorige Blogpost die Zahl der CPU-Zyklen eingeführt:
$ perf stat -e cycles:u,instructions:u ./prog
Performance counter stats for './prog':
1,001,300 cycles:u
3,000,005 instructions:u # 3.00 insn per cycle
Was ich jetzt gehofft hatte, war, dass cycles:u
relativ stabil
bleibt, egal, was sonst noch auf dem System los ist. (Diese Annahme
funktioniert nur bei rein CPU-gebundenen Aufgaben.)
Wenn Hyper-Threading aus ist, stimmt das auch. Ist es aber an und laufen im Hintergrund noch andere Dinge, dann sieht man das:
$ while true; do true; done ← das hier in ein paar anderen Terminals
$ perf stat -e cycles:u,instructions:u ./prog
Performance counter stats for './prog':
1,971,297 cycles:u
3,000,005 instructions:u # 1.52 insn per cycle
So ein Elend. Die Zahl der Instruktionen bleibt gleich, aber die CPU braucht jetzt mehr Zyklen, um sie auszuführen.
Ein Blick auf die volle Ausgabe von perf stat
liefert einen Hinweis:
$ perf stat ./prog
Performance counter stats for './prog':
0.61 msec task-clock:u # 0.641 CPUs utilized
0 context-switches:u # 0.000 /sec
0 cpu-migrations:u # 0.000 /sec
1 page-faults:u # 1.652 K/sec
2,020,909 cycles:u # 3.339 GHz
———————→ 1,212,002 stalled-cycles-frontend:u # 59.97% frontend cycles idle
3,000,005 instructions:u # 1.48 insn per cycle
# 0.40 stalled cycles per insn
1,000,002 branches:u # 1.652 G/sec
4 branch-misses:u # 0.00% of all branches
CPUs sind heute unglaublich komplex. Es wird jetzt ein bisschen schwer, herauszufinden, warum genau das passiert. Mir ist kein Weg bekannt, um mir anzuschauen, was die CPU da intern tut.
Es gibt diese Doku:
Da habe ich mich noch nicht komplett durchgegraben. Nach dem, was ich bisher verstehe, stand mein Programm für 1212002 Zyklen still, weil, naja, die CPU „irgendwas anderes“ gemacht hat. Was genau, weiß ich nicht. Wer das detailliert erklären kann, möge sich gerne melden.
Dieses Phänomen tritt nur auf, wenn
logical_cores / 2 - 1
laufende Hintergrundprozesse gibt.– Edit: Ursprünglich stand hier real_cores
, das war falsch. Gemeint
ist die Anzahl der logischen Cores, die man zum Beispiel in htop
sieht. Beispiel: 8 logische Cores in htop
mit aktivem HT → wenn mehr
als 3 Hintergrundprozesse aktiv sind, zeigt sich der Effekt.
time(1)
ist keine gute Metrik für „wie viel Arbeit hat die CPU
verrichtet“, um eine Aufgabe zu erledigen. Hintergrundprozesse
können Thermal-Throttling anstoßen und damit den zu messenden
Prozess negativ beeinflussen.perf stat -e cycles:u
ist auch nicht besonders gut, falls
Hyper-Threading an ist. „Hyper-Threads“ sind nunmal keine echten
Kerne und können die Ausführung anderer Prozesse dann auch wieder
negativ beeinflussen.cycles:u
hat wenig Aussagekraft. Man kann
daran nicht ablesen, wie viele dieser Zyklen die CPU gearbeitet hat
und wie viele sie verschwendet hat. Es ist lediglich die Zahl der
Zyklen, die unserem Prozess zugeschrieben wurden. stalled-cycles-frontend:u
abziehen, wenn Hyper-Threading aktiviert ist.cycles:u
ist wohl doch nicht „die Summe aller Cycles“, wie man zum
Beispiel an perf stat date
sehen kann, wo
stalled-cycles-frontend:u
größer als cycles:u
ist …Es ist wirklich nur die Spitze des Eisbergs. Es sind noch so viele
weitere Komponenten involviert. „Einfache Benchmarks“, wie ich sie
ursprünglich mit time(1)
durchführen wollte, sind deutlich
komplizierter, als man meinen mag, vor allem wenn das System mehr als
eine Sache zu einer Zeit tut (was im Jahre 2021 mit ziemlicher
Sicherheit der Fall ist).