GoSuda

Was ist eine Managed Language?

By Lee Yunjin
views ...

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 MaschinencodeZu speichernde DatenSprach-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.