GoSuda

Hva er et managed language?

By Lee Yunjin
views ...

Hva er et managed språk?

I motsetning til unmanaged språk, som kun utfører den logikken programmereren har skrevet uten vesentlige avvik, er et managed språk et språk som utfører GC, runtime-optimalisering, green threads, samt håndtering av concurrency i runtime, slik at brukeren slipper å utføre risikabel lavnivå-håndtering. For slike språk er fordelen at man kan fokusere utelukkende på forretningslogikk og fordype seg i utviklingen, men på den annen side kan det hende at programmet fungerer annerledes enn det programmererens intuisjon skulle tilsi, noe som av og til krever presis runtime-tuning. Først skal vi se på Go-språket, som er det mest trofaste mot minimalistisk filosofi og har en ærlig assembly blant managed språk.

Binærstruktur i Go-språket

.text.data.gopclntab, .typelink osv.
Maskinkode som skal utføresData som skal lagresSpråkets runtime-seksjon
Siden Go-språket ikke oversetter maskinkode 1:1 slik brukeren skriver det, er logikken i .text-seksjonen tett knyttet til språkets runtime-seksjon.
Videre legges funksjoner som runtime.printnl(), som brukeren ikke har skrevet eksplisitt, til i .text-seksjonens assembly.
Gjennom slik automatisk kodeinnsetting hjelper Go-språket utvikleren med å slippe manuell håndtering.

Se kun på main-funksjonen i Go

La oss først skrive et enkelt eksempel på kildekoden main.go og se på den fra main på en AMD64-maskin.

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

Deretter bygger vi den slik.

1go build main.go
2

Go støtter go tool for enkel lavnivå-debugging. For å se kun assembly for main-funksjonen i main-pakken i go tool, skriver vi inn følgende 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
  • For å sjekke om gjeldende goroutine-stakkrammeplass er tilstrekkelig, sammenlignes stack guard-verdien inne i goroutine-kontrollblokkens register (R14) med gjeldende stack pointer (SP) ved bruk av CMPQ, og hvis den er utilstrekkelig, hoppes det (JBE) til adressen 0x468f95, som er entrypoint for stakkutvidelse.
  • For å lagre den forrige base pointeren, settes den inn på stakken med PUSHQ BP.
  • Gjeldende stack pointer (SP) kopieres til base pointer (BP)-registeret for å låse stakkens referansepunkt ved funksjonsstart.
  • Deretter tildeles 16 bytes med lokal variabel-stakkplass (SUBQ $0x10, SP), og NOPL brukes til å fylle ut virtuelle instruksjoner for å utføre CPU cache alignment.
  • runtime.printlock(SB) kalles for å låse synkroniseringen av intern strengutskrift i Go-runtime.
  • Ved bruk av LEAQ-instruksjonen lagres startadressen til den konstante strengen ("Hello World") i AX, som er det første parameteret i henhold til Go ABI-spesifikasjonen blant de generelle registrene.
  • Deretter lagres verdien som representerer strengens lengde i det andre parameterregisteret, BX. (MOVL $0xb, BX, altså 11 i desimaltall)
  • runtime.printstring(SB) kalles for å skrive ut til konsollen basert på informasjonen i AX (dataadresse) og BX (lengde).
  • runtime.printnl(SB) kalles for linjeskift.
  • Når utskriften er fullført, frigjøres låsen via runtime.printunlock(SB).
  • Den tildelte 16-bytes stakkminnet gjenopprettes med ADDQ $0x10, SP.
  • Den tidligere base pointeren gjenopprettes med POPQ BP.
  • Kontrollen returneres til der funksjonen ble kalt via RET.
  • Hvis plassen var utilstrekkelig ved den første stakksjekken, kalles runtime.morestack_noctxt.abi0(SB) på adressen 0x468f95 for å utvide stakk-runtime dynamisk, slik det forventes av et managed språk.
  • Når stakkutvidelsen er fullført, returneres det (JMP) til inngangspunktet for main.main(SB). Som vist er assemblyen for forretningslogikken ganske tydelig, med kun lett runtime-håndtering lagt til.

Uten optimalisering

Formen ovenfor er resultatet av at Go-kompilatoren automatisk har inlinet to separate funksjoner for optimalisering. For læringsformål skal vi imidlertid sørge for at sayHello ikke inlines i dette tilfellet. For å oppnå dette kompileres kildekoden med følgende flagg.

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

Hvis vi skriver ut resultatene i skallet, oppdages duplisert 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, settes MOVQ-operasjoner inn for å laste verdiene på nytt til stakkpeker-offset som 0x20(SP) osv., for å bevare parametrene (AX, BX) i samsvar med funksjonskallspesifikasjonen. Det er dermed bekreftet at det kompilatoren optimaliserer, er slike unødvendige minneflyttingsoperasjoner og kall-overhead.

Neste gang

Neste gang skal vi ta for oss if-setninger og switch-setninger i Go-språket. Hvis tiden tillater det, vil vi også analysere Go-runtime-seksjonene senere.