Was ist eine Managed Language?
Was ist eine Managed Language?
Eine Managed Language ist im Gegensatz zu einer Unmanaged Language – also einer Sprache, die lediglich die vom Programmierer geschriebene Logik ohne wesentliche Abweichungen ausführt – eine Sprache, die GC, Runtime-Optimierungen, Green Threads, Nebenläufigkeitsverarbeitung und Ähnliches zur Laufzeit ausführt, sodass der Benutzer keine riskante Low-Level-Verwaltung vornehmen muss. Solche Sprachen bieten den Vorteil, dass man sich voll und ganz auf die Business Logic konzentrieren kann; andererseits kann es vorkommen, dass sich das Programm anders verhält, als es die Intuition des Programmierers nahelegt, weshalb manchmal eine präzise Runtime-Optimierung erforderlich ist. Zunächst betrachten wir die Go-Sprache, die unter den Managed Languages am konsequentesten eine minimalistische Philosophie verfolgt und deren Assembly-Code als transparent gilt.
Binärstruktur der Go-Sprache
| .text | .data | .gopclntab, .typelink usw. |
|---|---|---|
| Auszuführender Maschinencode | Zu speichernde Daten | Sprach-Runtime-Sektionen |
| Da Go den vom Benutzer eingegebenen Code nicht 1:1 in Maschinencode übersetzt, ist die Logik in der .text-Sektion eng mit den Sprach-Runtime-Sektionen verknüpft. | ||
| Darüber hinaus werden Funktionen wie runtime.printnl(), die vom Benutzer nicht explizit geschrieben wurden, in die Assembly der .text-Sektion eingefügt. | ||
| Durch diese automatische Code-Injektion unterstützt Go den Entwickler dabei, sich von der manuellen Verwaltung zu befreien. |
Betrachtung der main-Funktion in Go
Lassen Sie uns zunächst ein einfaches Beispiel-Sourcefile main.go erstellen und die main-Funktion auf einer AMD64-Maschine betrachten.
1package main
2
3func sayHello(msg string) {
4 println(msg)
5}
6
7func main() {
8 sayHello("Hello World")
9}
10
Danach führen wir den Build-Vorgang wie folgt durch:
1go build main.go
2
Go unterstützt für einfaches Low-Level-Debugging das Tool go tool. Um im go tool nur die Assembly für die main-Funktion des main-Pakets anzuzeigen, geben wir diesen Befehl ein:
1go tool objdump -s "main\.main" ./main
2
Assembly
1TEXT main.main(SB) /home/yjlee/compare-assembly/go/main.go
2 main.go:7 0x468f60 493b6610 CMPQ SP, 0x10(R14)
3 main.go:7 0x468f64 762f JBE 0x468f95
4 main.go:7 0x468f66 55 PUSHQ BP
5 main.go:7 0x468f67 4889e5 MOVQ SP, BP
6 main.go:7 0x468f6a 4883ec10 SUBQ $0x10, SP
7 main.go:8 0x468f6e 90 NOPL
8 main.go:4 0x468f6f e8cca3fcff CALL runtime.printlock(SB)
9 main.go:4 0x468f74 488d05da290100 LEAQ 0x129da(IP), AX
10 main.go:4 0x468f7b bb0b000000 MOVL $0xb, BX
11 main.go:4 0x468f80 e83bacfcff CALL runtime.printstring(SB)
12 main.go:4 0x468f85 e8f6a5fcff CALL runtime.printnl(SB)
13 main.go:4 0x468f8a e811a4fcff CALL runtime.printunlock(SB)
14 main.go:9 0x468f8f 4883c410 ADDQ $0x10, SP
15 main.go:9 0x468f93 5d POPQ BP
16 main.go:9 0x468f94 c3 RET
17 main.go:7 0x468f95 e8e6afffff CALL runtime.morestack_noctxt.abi0(SB)
18 main.go:7 0x468f9a ebc4 JMP main.main(SB)
19
- Es wird mittels CMPQ geprüft, ob der aktuelle Goroutine-Stack-Frame-Bereich ausreicht, indem der Stack-Guard-Wert innerhalb des Goroutine-Control-Block-Registers (R14) mit dem aktuellen Stack Pointer (SP) verglichen wird; falls der Speicher nicht ausreicht, erfolgt ein Sprung (JBE) zur Adresse 0x468f95, dem Entrypoint für die Stack-Erweiterung.
- Der vorherige Base Pointer wird mittels PUSHQ BP auf den Stack gelegt, um ihn zu sichern.
- Der aktuelle Stack Pointer (SP) wird in das Base Pointer (BP) Register kopiert, um den Referenzpunkt des Stacks zu Beginn der Funktion zu fixieren.
- Anschließend wird Stack-Speicher für lokale Variablen in der Größe von 16 Bytes reserviert (SUBQ $0x10, SP) und mittels NOPL werden virtuelle Befehle eingefügt, um das CPU-Cache-Alignment durchzuführen.
- Zur Synchronisation der internen Standardausgabe von Zeichenfolgen ruft die Go-Runtime runtime.printlock(SB) auf, um eine Sperre zu setzen.
- Mit dem Befehl LEAQ wird die Startadresse der als Konstante zugewiesenen Zeichenfolge ("Hello World") in AX gespeichert, einem der Universalregister, das gemäß Go-ABI-Spezifikation als erstes Argument verwendet wird.
- Danach wird der Wert, der die Länge der Zeichenfolge darstellt, im zweiten Parameterregister BX gespeichert. (MOVL $0xb, BX, also 11 im Dezimalsystem).
- runtime.printstring(SB) wird aufgerufen, um basierend auf den Informationen in AX (Datenadresse) und BX (Länge) die Ausgabe auf der Konsole zu tätigen.
- Für den Zeilenumbruch wird runtime.printnl(SB) aufgerufen.
- Da die Ausgabe abgeschlossen ist, wird die Sperre über runtime.printunlock(SB) wieder aufgehoben.
- Mit ADDQ $0x10, SP wird der zuvor reservierte 16-Byte-Stack-Speicher wieder freigegeben.
- Mit POPQ BP wird der ursprüngliche Base Pointer wiederhergestellt.
- Über RET wird die Kontrolle an den Aufrufer der Funktion zurückgegeben.
- Falls bei der anfänglichen Stack-Prüfung festgestellt wurde, dass der Platz nicht ausreicht, wird runtime.morestack_noctxt.abi0(SB) an Adresse 0x468f95 aufgerufen, um – wie es sich für eine Managed Language gehört – den Stack-Runtime-Bereich dynamisch zu erweitern.
- Nach Abschluss der Stack-Erweiterung erfolgt die Rückkehr (JMP) zum Einsprungpunkt von main.main(SB). Wie zu sehen ist, ist die Assembly der Business Logic recht übersichtlich und nur um eine schlanke Runtime-Verwaltung ergänzt.
Ohne Optimierung
Die oben gezeigte Form ist das Ergebnis der automatischen Inlining-Optimierung des Go-Compilers, bei der zwei getrennte Funktionen zusammengeführt wurden. Zu Lernzwecken werden wir jedoch in diesem Fall das Inlining von sayHello unterbinden. Dazu kompilieren wir den Sourcecode mit dem folgenden Flag:
1go build -gcflags="-l" main.go
2
Wenn wir das Ergebnis in der Shell ausgeben, lassen sich redundante Assembly-Anweisungen feststellen.
1yjlee@elegant:~/compare-assembly/go$ go build -gcflags="-l" main.go
2
3go tool objdump -s "main\.sayHello" ./main
4TEXT main.sayHello(SB) /home/yjlee/compare-assembly/go/main.go
5 main.go:3 0x468f60 493b6610 CMPQ SP, 0x10(R14)
6 main.go:3 0x468f64 7636 JBE 0x468f9c
7 main.go:3 0x468f66 55 PUSHQ BP
8 main.go:3 0x468f67 4889e5 MOVQ SP, BP
9 main.go:3 0x468f6a 4883ec10 SUBQ $0x10, SP
10 main.go:5 0x468f6e 4889442420 MOVQ AX, 0x20(SP)
11 main.go:5 0x468f73 48895c2428 MOVQ BX, 0x28(SP)
12 main.go:4 0x468f78 e8c3a3fcff CALL runtime.printlock(SB)
13 main.go:4 0x468f7d 488b442420 MOVQ 0x20(SP), AX
14 main.go:4 0x468f82 488b5c2428 MOVQ 0x28(SP), BX
15 main.go:4 0x468f87 e834acfcff CALL runtime.printstring(SB)
16 main.go:4 0x468f8c e8efa5fcff CALL runtime.printnl(SB)
17 main.go:4 0x468f91 e80aa4fcff CALL runtime.printunlock(SB)
18 main.go:5 0x468f96 4883c410 ADDQ $0x10, SP
19 main.go:5 0x468f9a 5d POPQ BP
20 main.go:5 0x468f9b c3 RET
21 main.go:3 0x468f9c 4889442408 MOVQ AX, 0x8(SP)
22 main.go:3 0x468fa1 48895c2410 MOVQ BX, 0x10(SP)
23 main.go:3 0x468fa6 e8d5afffff CALL runtime.morestack_noctxt.abi0(SB)
24 main.go:3 0x468fab 488b442408 MOVQ 0x8(SP), AX
25 main.go:3 0x468fb0 488b5c2410 MOVQ 0x10(SP), BX
26 main.go:3 0x468fb5 eba9 JMP main.sayHello(SB)
27
Nach dem Deaktivieren des Inlinings werden MOVQ-Operationen eingefügt, um die Werte (AX, BX) bei den Stack-Pointer-Offsets wie 0x20(SP) erneut zu speichern, damit sie gemäß der Funktionsaufruf-Konvention erhalten bleiben. Es ist somit bestätigt, dass der Compiler insbesondere solche unnötigen Speicherverschiebungen und Aufruf-Overheads optimiert.
Nächstes Mal
Im nächsten Abschnitt werden wir uns mit if-Statements und switch-Statements in Go befassen. Sollte die Zeit es erlauben, werden wir zu einem späteren Zeitpunkt auch die Go-Runtime-Sektionen analysieren.