blog · git · desktop · images · music · contact & privacy · gopher


Spitze des Eisbergs: perf stat und Hyper-Threading

2021-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:

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:

https://software.intel.com/content/www/us/en/develop/documentation/vtune-cookbook/top/methodologies/top-down-microarchitecture-analysis-method.html

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

  1. Hyper-Threading im BIOS aktiviert ist,
  2. es mehr als 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.

(Vorläufiges) Fazit

  1. 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.
  2. 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.
  3. Der nackte Wert von 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. Man sollte vermutlich mindestens den Wert von stalled-cycles-frontend:u abziehen, wenn Hyper-Threading aktiviert ist. Leider nein, 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).

Comments?