Hva er et managed language?
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øres | Data som skal lagres | Språ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.