In Hardware implementierte Performance-Zähler sind nicht nur bei x86-Prozessoren gängig, sondern auch in ARM-CPUs. Der Raspberry Pi eignet sich hierfür als – etwas störrisches – Testobjekt.
Viele Raspberry-Pi-Entwickler wissen nicht, dass auch ARM-Prozessoren eine Performance Monitoring Unit (PMU) besitzen. Profiler zum Beispiel könnten damit die Abarbeitungszeiten ausgewählter Programmteile messen, auch mancher Benchmark holt sich bei einer PMU ein genaues Zeitmaß. Drittens hilft sie Betriebssystemen, die unter Echtzeitanforderungen stehen, Zeitspannen in den Griff zu bekommen.
Der beim Raspberry 1 eingesetzte ARM1176 (ARMv6-Architektur) stellt drei Ereigniszähler bereit (Abbildung 1): Cycle Counter Register (CCR), Count Register 0 (CR0) sowie Count Register 1 (CR1). Während sich das Cycle Count Register wie der Time Stamp Counter (TSC) bei einer x86-CPU mit jedem Prozessortakt erhöht, zählen die beiden anderen Register abhängig von der Konfiguration unterschiedliche Ereignisse.
Hier stehen die Gesamtzahl der ausgeführten Befehle, die Anzahl der Sprungbefehle, der Unterprogramm-Aufrufe oder der Cache-Misses bei Instruktionen und Datenzugriffen zur Wahl (Tabelle 2). Insgesamt 21 Ereignisse kann der Programmierer hier entdecken.
Tabelle 2
Ereignisquellen für die Zähler CR0, CR1 [1]
| Quelle | Ereignis |
|---|---|
| 0x0 | Instruction Cache Miss |
| 0x1 | Instruction Buffer cannot deliver Instruction |
| 0x2 | Data dependency stall |
| 0x3 | Instruction Micro TLB Miss |
| 0x4 | Data Micro TLB Miss |
| 0x5 | Branch Instruction executed |
| 0x6 | Branch mispredicted |
| 0x7 | Instructions executed |
| 0x9 | Data Cache Access (nonsequential, cacheable only) |
| 0xA | Data Cache Access (nonsequential, cacheable and noncacheable) |
| 0xB | Data Cache Miss |
| 0xC | Data Cache write-back |
| 0xD | Software changed the PC |
| 0xF | Main TLB Miss |
| 0x10 | Explicit external Data Access |
| 0x11 | Load Store Unit Request Queue full |
| 0x12 | Write Buffer drained due to Synchronization |
| 0x20 | Embedded Trace Macrocell 0 external Out Event |
| 0x21 | Embedded Trace Macrocell 1 external Out |
| 0x22 | Embedded Trace Macrocell both |
| 0x23 | Procedure Call Instruction executed |
| 0x24 | Procedure Return Instruction executed |
| 0x25 | Return Address predicted correctly |
| 0x26 | Return Address predicted incorrectly |
| 0xFF | Cycles |
Showdown nach 6 Sekunden
Die drei Zähler sind 32 Bit breit, können also rund vier Milliarden Ereignisse zählen, bis es zu einem Überlauf kommt. Bei einer Taktfrequenz von 700 MHz sind diese für das CCR bereits nach rund 6 Sekunden bei einer Genauigkeit von 1,43 Mikrosekunden erreicht, bei einem mit 800 MHz übertakteten Raspberry Pi fällt bereits nach rund 5 1/2 Sekunden der Vorhang. Die Genauigkeit liegt dann bei 1,25 Mikrosekunden.
Mit einem Überlauf beginnen allerdings die Probleme. Auch wenn die Beschreibung in der technischen Dokumentation zum Prozessor des Performance Monitoring Control Register (PMCR, Tabelle 1) suggeriert, dass sich im Fall eines Überlaufs ein Interrupt auslösen ließe, unterstützt das der im Raspberry Pi verbaute ARM11 nicht. Das Kernelsubsystem »perf« , das für den Umgang mit Performance-Zählern zuständig ist, erwartet hier aber Interrupts. Hier liegt eine der Ursachen für die anfangs mangelnde Unterstützung durch den Linux-Kernel.
Tabelle 1
Bedeutung der Bits im PMCR
| Bits | Feldname | Funktion |
|---|---|---|
| 28-31 | SBZ | “Should Be Zero” (reserviert) |
| 27-20 | EvtCount0 | Ereignisquelle für CR0 wählen |
| 19-12 | EvtCount1 | Ereignisquelle für CR1 wählen |
| 11 | X | Events auf den Event-Bus exportieren |
| 10 | CCR | CCR-Überlauf-Flag (zurücksetzen durch Schreiben einer 1) |
| 9 | CR1 | CR1-Überlauf-Flag (zurücksetzen durch Schreiben einer 1) |
| 8 | CR0 | CR0-Überlauf-Flag (zurücksetzen durch Schreiben einer 1) |
| 7 | SBZ | “Should Be Zero” (reserviert) |
| 6 | ECC | CCR-Überlauf-Interrupt freigeben (nicht unterstützt) |
| 5 | EC1 | CR1-Überlauf-interrupt freigeben (nicht unterstützt) |
| 4 | EC0 | CR0-Überlauf-interrupt freigeben (nicht unterstützt) |
| 3 | D | Skalieren von CCR um 64 |
| 2 | C | CCR löschen |
| 1 | P | CR0 und CR1 löschen |
| 0 | E | Freigeben der Zähler CCR, CR0 und CR1 |
Selbst ist der Entwickler
Linux wäre nicht Linux, wenn pfiffige Programmierer die Funktionalität nicht nachrüsten könnten: Sie greifen mit Befehlen selbst auf die PMU zu, konfigurieren im PMCR die zu zählenden Ereignisse und geben schließlich die Zähler frei. Für die Arbeit mit dem Register gibt es mit »MCR« (Move to Coproc from ARM Reg) und »MRC« (Move to ARM Reg from Coproc) extra Assemblerbefehle, die erwartungsgemäß nur im privilegierten Modus, also im Kernel, erlaubt sind.
Überraschenderweise lässt sich genau dies aber ändern! Dafür haben die Ingenieure dem Prozessor das “Secure User and non-secure Access Validation Control Register” (SUNAVCR) spendiert ([1], 3-132). Ist in diesem Register das Bit 0 gesetzt, greift eine normale Applikation auf die Register der PMU zu, ohne mit einer Exception abgestraft zu werden. Das Setzen des Bits im SUNAVCR ist natürlich nur im privilegierten Modus möglich, sodass niemand um das Schreiben eines kleinen Kernelmoduls herumkommt. Dies macht aber der Weg für eine Applikation frei, die Performance-Informationen zu den implementierten Codesequenzen sammelt.
Listing 1 zeigt den übersichtlichen Quellcode des Kernelmoduls. Beim Laden des Moduls setzt Zeile 21 Bit 0 des SUNAVCR mit der Assembleranweisung in Zeile 14 auf 1 und Zeile 36 beim Entladen auf 0. Das war’s schon. Der Aufbau der Assembleranweisung selbst ist mit ihren vielen Parametern etwas komplexer – sie gehört zum System Control Coprocessor und interessiert darüber hinaus hier nicht.
Listing 1
pmu.c – Kernelmodul für den PMU-Zugriff
01 #include <linux/kernel.h>
02 #include <linux/module.h>
03
04 static inline u32 armv6_sunavcr_read(void)
05 {
06 u32 val;
07
08 asm volatile("mrc p15, 0, %0, c15, c9, 0" : "=r"(val));
09 return val;
10 }
11
12 static inline void armv6_sunavcr_write(u32 val)
13 {
14 asm volatile("mcr p15, 0, %0, c15, c9, 0" : : "r"(val));
15 }
16
17 static int __init pmu_init(void)
18 {
19 u32 sunavcr = 0;
20
21 armv6_sunavcr_write(0x1);
22 sunavcr = armv6_sunavcr_read();
23
24 if (sunavcr!=0) {
25 printk("pmu_init(): userland acces to pmu-regs activated\n");
26 return 0;
27 } else {
28 printk("pmu_init(): sunavcr_write() failed\n");
29 return -EIO;
30 }
31 }
32
33 static void __exit pmu_exit(void)
34 {
35 printk ("pmu_exit()\n");
36 armv6_sunavcr_write(0x0);
37 }
38
39 module_init(pmu_init);
40 module_exit(pmu_exit);
41
42 MODULE_LICENSE("GPL");
Kernelheader notwendig
Unglücklicherweise können Raspberry-Pi-Besitzer das Modul unter der weit verbreiteten Variante Raspbian nicht ohne Weiteres anfertigen. Hierzu sind die zum Kern gehörenden Kernelheader- und Konfigurations-Dateien notwendig, die kein einfaches »apt-get« aus dem Standard-Repository zu installieren in der Lage wäre. Vielmehr ist es notwendig, zunächst mit dem Kommando »uname -r« die exakte Kernelversion zu identifizieren und mit dieser Kenntnis das zugehörige Debian-Paket von der Webseite [4] herunterzuladen (Abbildung 3).
Ist das Paket per »dpkg -i« installiert, muss der Admin einen symbolischen Link auf die neu installierten Dateien per »ln -s« legen. Ein nachfolgendes »make« erzeugt (infolge des Makefile in Listing 2) aus dem Quellcode »pmu.c« das Kernelmodul »pmu.ko« , »insmod pmu.ko« lädt es. Ob das Laden erfolgreich war, lässt sich über das Kommando »tail -f/var/log/messages« verifizieren.
Listing 2
Makefile zum Kernelmodul
01 ifneq ($(KERNELRELEASE),) 02 obj-m := pmu.o 03 04 else 05 KDIR := /lib/modules/$(shell uname -r)/build 06 PWD := $(shell pwd) 07 08 default: 09 $(MAKE) -C $(KDIR) M=$(PWD) modules 10 endif 11 12 clean: 13 rm -rf *.o *.ko modules.order Module.symvers 14 rm -rf .*.cmd .tmp_versions *.mod.c
Abbildung 4 zeigt den Vorgang für den beim Schreiben des Artikels aktuellen Raspbian-Kernel 3.18.7+. Die einzugebenden Kommandos sind gelb unterlegt. Im Screenshot ist das Kommando »dpkg« nur kommentiert dargestellt, weil auf dem verwendeten System die Kernelquellen bereits installiert waren.
Sobald das Modul den Zugriff auf die PMU-Register gestattet, kann eine Applikation sich ihrer bedienen. Los geht es mit dem PMCR, das Register ist 32 Bit breit und lässt sich lesen und beschreiben. Das Bit 1 (für CR0 und CR1) und Bit 2 (für CCR) setzen die Zähler auf 0. Die Ereignisquellen für den Zähler CR0 legen die Bits 20 bis 27 fest, für CR1 die Bits 12 bis 19. Wer will, verpasst an dieser Stelle dem Taktzyklen-Register CCR den Skalierungsfaktor 64. Er reduziert die Genauigkeit der Messung auf etwas besser als eine Millisekunde zugunsten der Länge der Messung – immerhin 392 Sekunden darf sie maximal laufen.
Einen danach auftretenden Überlauf signalisiert Bit 10; die Bits 8 und 9 zeigen bei lesendem Zugriff des PMCR einen Überlauf der Register CR0 und CR1 an. Um die Überlauf-Bits zu löschen, muss der Programmierer die zugehörige Bitposition im PMCR jeweils mit einer 1 (keiner 0) belegen. Das Schreiben einer »1« an der Bitposition 0 ist schließlich für die eigentliche Aktivierung der drei Zähler verantwortlich.
Konfig als Hexzahl
Die Kodierung der Ereignisquellen zeigt Tabelle 2. Um beispielsweise den Zähler CR0 für das Zählen der abgearbeiteten Befehle einer Codesequenz und Zähler CR1 für das Zählen von Sprungbefehlen zu nutzen, müssen die Bits 20 bis 27 den Wert 7 und die Bits 12 bis 19 den Wert 5 haben. Sind in Register CR0 die Instruction Cache Misses und in Register CR1 die Data Cache Misses zu zählen, wird der Programmierer an die Bitpositionen 20 bis 27 den Wert 0x0 und in die Bits 12 bis 19 den Wert 0xb schreiben. Typischerweise wird er gleichzeitig mit diesen Einstellungen auch die Zähler auf 0 setzen, die Überlauf-Bits löschen und die Messung aktivieren (Abbildung 2).
Für den ersten Fall ergibt sich 0x00705707 als Wert für das Konfigurationsregister, im zweiten Fall 0x0000b707. Listing 3 stellt die Beispielapplikation »readregs.c« vor, die die Anzahl der abgearbeiteten Instruktionen, die Anzahl der Instruction Cache Misses und die benötigten Taktzyklen einer kurzen Befehlssequenz – einer Schleife von 1 bis 10 – ausmisst. Das Kompilieren gelingt mit dem Kommando »make CFLAGS=”-g -Wall” readregs« . Dieser Vorgang, der nachfolgende Aufruf und das Ergebnis der Ausführung ist in Abbildung 4 zu besichtigen.
Listing 3
readregs.c misst Parameter einer Schleife
01 #include <stdio.h>
02 #include <stdint.h>
03
04 #define armv6_read_ccr( val ) \
05 asm volatile("mrc p15, 0, %0, c15, c12, 1" : "=r"(val))
06 #define armv6_read_cr0( val ) \
07 asm volatile("mrc p15, 0, %0, c15, c12, 2" : "=r"(val))
08 #define armv6_read_cr1( val ) \
09 asm volatile("mrc p15, 0, %0, c15, c12, 3" : "=r"(val))
10 #define armv6_read_pmcr( val ) \
11 asm volatile("mrc p15, 0, %0, c15, c12, 0" : "=r"(val))
12 #define armv6_write_pmcr( val ) \
13 asm volatile("mcr p15, 0, %0, c15, c12, 0" : : "r"(val))
14
15 int main( int argc, char** argv, char** envp )
16 {
17 uint32_t cr1;
18 uint32_t before_ccr, after_ccr;
19 uint32_t before_cr0, after_cr0;
20 uint32_t before_cr1, after_cr1;
21 unsigned int i;
22
23 armv6_read_cr1( cr1 );
24 printf("PMCR=%x\n", cr1 );
25
26 armv6_write_pmcr( 0x00705707 );
27 armv6_read_cr1( before_cr1 );
28 armv6_read_ccr( before_ccr );
29 armv6_read_cr0( before_cr0 );
30 // Beginn auszumessende Codesequenz
31 for (i=0; i<10; i++ )
32 ;
33 // Ende auszumessende Codesequenz
34 armv6_read_cr0( after_cr0 );
35 armv6_read_ccr( after_ccr );
36 armv6_read_cr1( after_cr1 );
37
38 printf("ccr: %d (before: %d after: %d) CYCLES\n",
39 after_ccr-before_ccr, before_ccr, after_ccr);
40 printf("cr0: %d (before: %d after: %d) INSTRUCTIONS\n",
41 after_cr0-before_cr0, before_cr0, after_cr0);
42 printf("cr1: %d (before: %d after: %d) BRANCHES\n",
43 after_cr1-before_cr1, before_cr1, after_cr1);
44
45 armv6_read_cr1( cr1 );
46 printf("PMCR=%x\n", cr1 );
47 return 0;
48 }
Für die Ausführung der Schleife (plus Auslesen der Register CR0 und CR1) hatte die Himbeere im Test 452 Taktzyklen benötigt. Bei einer Taktfrequenz von 700 MHz benötigt der Raspberry dafür also 646 Mikrosekunden. In dieser Zeit hat er 68 Instruktionen abgearbeitet, davon zwölf Sprungbefehle.
Abbildung 5 zeigt den zur Codesequenz gehörenden Assemblercode. Die eigentliche Schleife ist grün unterlegt, das Auslesen der Register gelb. Weitere Beispiele zur Anwendung der PMU auf dem Raspberry Pi finden sich bei [2].
Spaßbremse ARM 11
Wer die Performance Monitoring Unit (PMU) auf dem Raspberry Pi benutzen will, hat leider einiges zu beachten. Erstens kann er neben dem normalen Taktzyklenzähler zu einem Zeitpunkt mit den beiden Zählern CR0 und CR1 maximal zwei Ereignisse protokollieren. Zweitens hat er zu gewährleisten, dass nicht mehrere Rechenprozesse zeitgleich auf das Konfigurationsregister PMCR schreibend zugreifen, und drittens, dass es bei den Zählern zu keinem Überlauf kommt. Am schwersten aber wiegt der vierte Punkt: Nach einem Prozesswechsel (Context Switch) sind die Werte der PMU-Register nicht mehr verlässlich.
All die Einschränkungen lassen sich nur überwinden, wenn der Kernel die PMU des Raspbery Pi unterstützt. Dafür gibt es erste Arbeiten zu melden, die zum Teil schon im Vanilla-Linux-Kernel eingeflossen sind [3]. Als Ziel steht die PMU über das Standardinterface des »perf« -Subsystems den Applikationen zur Verfügung – vielleicht das lohnenswerte Thema einer anderen Kern-Technik.
Raspberry Pi 2
Die Raspberry Foundation hat im Februar mit dem Verkauf der Version 2 begonnen, die bei den Anschlüssen und in den Abmessungen der Version 1 gleicht, in Sachen ARM-Generation aber zeitgemäßer ausfällt (ARMv7- statt ARMv6-Architektur). Die Foundation verspricht “vollständige Kompatibilität”.
Nach Tests im Zuge dieses Artikels lässt sich das Versprechen nicht halten. So funktionierte der vorgestellte Zugriff auf die PMU-Register wenig überraschend nur auf Raspberry Pis der ersten Generation. Die Kernel für Generation 1 und 2 sind ohnehin nicht kompatibel, unterschiedlich zu konfigurieren und benötigen eigene Device-Trees.
Bei den Treibern fällt auf, dass sich Hardware-Adressen geändert haben. Den Bootloader U-Boot vom Raspberry Pi 2 in Betrieb zu nehmen erwies sich als äußerst mühsam. Die versprochene Kompatibilität scheint sich hauptsächlich auf Anwendungsebene und die Pinbelegung der Steckerleisten zu beziehen.
Infos
- ARM1176JZF-S Technical Reference Manual: http://infocenter.arm.com/help/topic/com.arm.doc.ddi0301h/DDI0301H_arm1176jzfs_r0p7_trm.pdf
- Paul Drongowski, “Performance counterkernel module”: http://sandsoftwaresound.net/raspberry-pi/performance-counter-kernel-module/
- Paradis, Weaver, “Enabling Raspberry Pi Performance Counter Support on Linux perf_event”: UMain ECE Tech Report 2014-2, http://web.eece.maine.edu/~vweaver/projects/perf_events/rasp-pi/paradis_ece599.pdf
- Archiv für Raspbian-Kernel-Headerdateien: http://www.niksula.hut.fi/~mhiienka/Rpi/linux-headers-rpi/










