GoSuda

Hvad er et managed language?

By Lee Yunjin
views ...

Hvad er et managed sprog?

Et managed sprog er, i modsætning til et unmanaged sprog – altså et sprog, der blot udfører den logik, programmøren har skrevet, uden væsentlig afvigelse – et sprog, der eksekverer GC, runtime-optimering, green threads, concurrency-håndtering og lignende i sin runtime, hvilket eliminerer behovet for, at brugeren foretager risikabel low-level styring. For sådanne sprog er fordelen, at man kan fokusere udelukkende på forretningslogik og fordybe sig i udviklingen, men på den anden side kan programmørens intuition og den faktiske programafvikling afvige, hvilket undertiden gør sofistikeret runtime-tuning nødvendig. Først vil vi betragte Go-sproget, som er det sprog blandt de managed sprog, der er mest tro mod minimalistisk filosofi og har en ærlig assembly.

Go-sprogets binære struktur

.text.data.gopclntab, .typelink osv.
Maskinkode til eksekveringData til lagringSprog-runtime-sektion
Da Go-sproget ikke oversætter maskinkode 1:1 efter brugerens input, er logikken i .text-sektionen tæt forbundet med sprogets runtime-sektion.
Desuden tilføjes funktioner, som brugeren ikke selv har skrevet, såsom runtime.printnl(), til .text-sektionens assembly.
Gennem denne automatiske kodeindsættelse hjælper Go-sproget udvikleren med at slippe for manuel styring.

Betragtning af kun main-funktionen i Go

Lad os først skrive et simpelt eksempel-source main.go og se på main fra en AMD64-maskine.

 1package main
 2
 3func sayHello(msg string) {
 4    println(msg)
 5}
 6
 7func main() {
 8    sayHello("Hello World")
 9}
10

Derefter bygges det således.

1go build main.go
2

Go understøtter go tool til nem low-level debugging. For kun at se assemblyen svarende til main-funktionen i main-pakken via go tool, indtastes denne kommando.

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
  • Efter at have sammenlignet stack-guard-værdien i goroutine-kontrolblokkens register (R14) med den aktuelle stack pointer (SP) ved hjælp af CMPQ for at se, om den nuværende goroutine-stackramme er tilstrækkelig, springes der (JBE) til adressen 0x468f95, som er entrypoint for stack-udvidelse, hvis pladsen er utilstrækkelig.
  • For at gemme den forrige base pointer indsættes den på stacken med PUSHQ BP.
  • Den aktuelle stack pointer (SP) kopieres til base pointer (BP)-registeret for at fastlåse stack-referencepunktet ved funktionens start.
  • Derefter allokeres 16 bytes stack-plads til lokale variabler (SUBQ $0x10, SP), og der udføres CPU-cache-alignment ved at udfylde med virtuelle instruktioner via NOPL.
  • runtime.printlock(SB) kaldes for at låse synkroniseringen af intern streng-standardoutput i Go-runtime.
  • Ved hjælp af LEAQ-instruktionen gemmes startadressen på den konstant tildelte streng ("Hello World") i AX, som ifølge Go ABI-specifikationen bruges som den første parameter blandt de generelle registre.
  • Derefter gemmes værdien, der repræsenterer strengens længde, i det andet parameterregister BX. (MOVL $0xb, BX, svarende til 11 decimalt).
  • runtime.printstring(SB) kaldes for at udskrive til konsollen baseret på AX (dataadresse) og BX (længde).
  • runtime.printnl(SB) kaldes for at håndtere linjeskift.
  • Da udskrivningen er fuldført, frigives låsen via runtime.printunlock(SB).
  • Den allokerede 16-byte stack-hukommelse gendannes med ADDQ $0x10, SP.
  • Den oprindelige base pointer gendannes med POPQ BP.
  • Kontrollen returneres til det punkt, hvor funktionen blev kaldt, via RET.
  • Hvis der ikke var tilstrækkelig plads ved den indledende stack-kontrol, kaldes runtime.morestack_noctxt.abi0(SB) på adressen 0x468f95 for at udvide stack-runtime dynamisk, som det sømmer sig for et managed sprog.
  • Når stack-udvidelsen er fuldført, vendes der tilbage (JMP) til entrypoint for main.main(SB). Som det ses, er forretningslogikkens assembly ganske overskuelig, og kun let runtime-styring er tilføjet.

Uden optimering

Ovenstående form er resultatet af, at Go-compileren automatisk har inlinet to separate funktioner og optimeret dem. Men for læringens skyld vil vi i dette tilfælde undlade at inline sayHello. For at gøre dette kompileres kilden med følgende flag.

1go build -gcflags="-l" main.go
2

Hvis resultatet undersøges i shellen, findes duplikeret assembly.

 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

Når inlining deaktiveres, indsættes MOVQ-operationer for at genindlæse værdier til stack pointer-offsettet 0x20(SP) osv., for at bevare parametrene (AX, BX) i overensstemmelse med funktionskalds-konventionen. Det er hermed bekræftet, at compilerens optimering retter sig mod netop sådanne unødvendige hukommelsesflytnings-operationer og kald-overhead.

Næste gang

Næste gang vil vi gennemgå if-sætninger og switch-sætninger i Go-sproget. Hvis tiden tillader det senere, vil jeg også analysere Go runtime-sektionerne.