GoSuda

Vad är ett managed language?

By Lee Yunjin
views ...

Vad är ett managed language?

Ett managed language är, till skillnad från ett unmanaged language – det vill säga ett språk som endast exekverar den logik som programmeraren har skrivit utan större avvikelser – ett språk som exekverar GC, runtime-optimeringar, green threads, samt hantering av concurrency i runtime, vilket befriar användaren från behovet av riskabel low-level-hantering. I fall av sådana språk finns fördelen att man kan fokusera uteslutande på affärslogik och fördjupa sig i utvecklingen, men å andra sidan kan det innebära att programmet beter sig annorlunda än vad programmerarens intuition förutsäger, vilket ibland kräver sofistikerad runtime-tuning. Först ska vi betrakta Go, ett språk bland de managed languages som är mest troget en minimalistisk filosofi och vars assembly är transparent.

Binär struktur i Go

.text.data.gopclntab, .typelink etc.
Maskinkod som ska exekverasData som ska lagrasSektioner för språkets runtime
Eftersom Go inte översätter maskinkod i ett 1:1-förhållande enligt användarens input, är logiken i .text-sektionen nära relaterad till sektionerna för språkets runtime.
Dessutom läggs funktioner som runtime.printnl(), vilka användaren inte skrivit explicit, till i .text-sektionens assembly.
Genom denna automatiska kodinjektion hjälper Go utvecklare att slippa manuell hantering.

Att granska endast main-funktionen i Go

Låt oss först skapa ett enkelt exempel, main.go, och granska det från 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

Därefter bygger vi det på följande sätt:

1go build main.go
2

Go tillhandahåller go tool för enkel low-level-debugging. För att endast se assemblyn för main-funktionen i main-paketet med go tool, anger vi följande 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 att ha kontrollerat om det aktuella goroutine-stackutrymmet är tillräckligt genom att jämföra stack-guard-värdet i goroutine-kontrollblocket (R14) med den aktuella stackpekaren (SP) via CMPQ, hoppar (JBE) programmet till adressen 0x468f95, vilket är en entrypoint för stackexpansion om utrymmet är otillräckligt.
  • För att spara föregående base pointer skjuts den in på stacken med PUSHQ BP.
  • Genom att kopiera den aktuella stackpekaren (SP) till base pointer-registret (BP) fixeras stackens referenspunkt vid funktionsstarten.
  • Därefter allokeras 16 byte stackutrymme för lokala variabler (SUBQ $0x10, SP), och NOPL används för att fylla ut med virtuella instruktioner för CPU-cache-alignment.
  • För att synkronisera standardutmatning av interna strängar i Go-runtime anropas runtime.printlock(SB) för att sätta ett lås.
  • Med hjälp av instruktionen LEAQ lagras startadressen för den konstantallokerade strängen ("Hello World") i AX, vilket är det första parametervärdet enligt Go ABI-specifikationen för generella register.
  • Därefter lagras strängens längd i det andra parameterregistret, BX. (MOVL $0xb, BX, vilket motsvarar 11 i decimal form).
  • runtime.printstring(SB) anropas för att skriva ut till konsolen baserat på AX (dataadress) och BX (längd).
  • runtime.printnl(SB) anropas för att hantera radbrytning.
  • När utmatningen är klar frigörs låset genom runtime.printunlock(SB).
  • Den allokerade stackminnesmängden på 16 byte återställs med ADDQ $0x10, SP.
  • Den tidigare base pointer-adressen återställs med POPQ BP.
  • Kontrollen återlämnas till anropspunkten via RET.
  • Om utrymmet var otillräckligt vid den initiala stackkontrollen anropas runtime.morestack_noctxt.abi0(SB) vid adress 0x468f95 för att dynamiskt expandera stack-runtime, vilket är karakteristiskt för ett managed language.
  • När stackexpansionen är slutförd återgår exekveringen (JMP) till ingångspunkten för main.main(SB). Som synes är assemblyn för affärslogiken relativt tydlig, med endast lätt runtime-hantering tillagd.

Vid frånvaro av optimering

Ovanstående form är resultatet av att Go-kompilatorn automatiskt har utfört inlining och optimerat de två separata funktionerna. För inlärningsändamål kommer vi dock i detta fall att förhindra inlining av sayHello. För att uppnå detta kompileras källkoden med följande flagga:

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

Om vi inspekterar resultatet i skalet upptäcks redundant 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 inaktiveras infogas MOVQ-operationer för att återlagra värden till stackpekarens offset, såsom 0x20(SP), i syfte att bevara parametrarna (AX, BX) i enlighet med funktionsanropskonventionen. Det bekräftas alltså att kompilatorns optimeringar riktar sig mot att eliminera sådana onödiga minnesförflyttningar och anrops-overhead.

Nästa gång

Nästa gång kommer vi att behandla if- och switch-satser i Go-språket. Om tid finnes framöver kommer även Go-runtime-sektionerna att analyseras.