Mitä on Managed Language?
Mitä ovat managed-kielet?
Managed-kielet eroavat unmanaged-kielistä – eli kielistä, joissa ohjelmoijan laatima logiikka suoritetaan suoraan ilman merkittäviä muutoksia – siten, että ne suorittavat runtime-ympäristössä GC:tä (Garbage Collection), runtime-optimointeja, green thread -hallintaa ja konkurentsikäsittelyä, jolloin käyttäjän ei tarvitse huolehtia vaarallisesta low-level-hallinnasta.
Tällaisten kielten etuna on se, että kehittäjä voi keskittyä täysin liiketoimintalogiikkaan, mutta toisaalta ohjelmoijan intuitio ja ohjelman todellinen toiminta saattavat poiketa toisistaan, mikä voi joskus edellyttää hienojakoista runtime-viritystä.
Tarkastelemme ensin Go-kieltä, joka on managed-kielistä uskollisin minimalistiselle filosofialle ja jonka assembly on suoraviivaista.
Go-kielen binäärirakenne
| .text | .data | .gopclntab, .typelink jne. |
|---|---|---|
| Suoritettava konekielinen koodi | Tallennettava data | Kielen runtime-osiot |
Koska Go-kieli ei käännä käyttäjän syötettä 1:1 konekielelle, .text-osion logiikka on tiiviissä yhteydessä kielen runtime-osioihin.
Lisäksi .text-osion assemblyyn lisätään funktioita, kuten runtime.printnl(), joita käyttäjä ei ole itse kirjoittanut.
Tämän automaattisen koodin lisäyksen avulla Go-kieli auttaa vapauttamaan kehittäjän manuaalisesta hallinnasta.
Go-kielen main-funktion tarkastelu
Luodaan ensin yksinkertainen esimerkkilähdekoodi main.go ja tarkastellaan main-funktiota AMD64-arkkitehtuurissa.
1package main
2
3func sayHello(msg string) {
4 println(msg)
5}
6
7func main() {
8 sayHello("Hello World")
9}
Tämän jälkeen suoritetaan build:
1go build main.go
Go tukee go tool -työkalua helpottamaan low-level-debuggausta.
Voimme tarkastella main-paketin main-funktion assemblya syöttämällä seuraavan komennon:
1go tool objdump -s "main\.main" ./main
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)
- Tarkistetaan CMPQ-käskyllä, onko nykyiseen säikeeseen päästy käsiksi; jos on, hypätään Entrypoint-kohtaan 0x468f95.
- Asetetaan sisääntulopiste pinoon
PUSHQ BP-käskyllä. - Määritetään funktion alussa pinon alkukohta viimeisimpään dataan viittaavaan SP-rekisteriin, jotta paikallisten muuttujien viittauspiste pysyy kiinteänä.
- Sen jälkeen varataan 16 tavua pinoa paikallisille muuttujille (
SUBQ $0x10, SP) ja käytetään NOPL-käskyä täyttämään tavuja CPU-välimuistin tasaamiseksi. - Go Runtime lukitsee merkkijonopuskurin tulostuksen kutsumalla funktiota
runtime.printlock(SB). - LEAQ-käskyllä tallennetaan varatun merkkijonon alkuosoite AX-rekisteriin (akkumulaattori), jota käytetään datan tallentamiseen.
- Tämän jälkeen tallennetaan merkkijonon pituus 11 BX-rekisteriin, jota käytetään laskennan apuna ja väliaikaisen datan tallennukseen (
MOVL $0Xb, BX). - Tulostetaan akkumulaattorin sisältö SB-puolelle
runtime.printstring(SB)-kutsulla. - Kirjoitetaan rivinvaihto SB-puolelle
runtime.printnl(SB)-kutsulla. - Vapautetaan merkkijonopuskuri
runtime.printunlock(SB)-kutsulla. - Palautetaan lainattu 16 tavun pinomuisti
ADDQ $0x10, SP-käskyllä. Koska sisääntulopiste asetettiin pinoon alussa, se poistetaan sieltäPOPQ BP-käskyllä ennen paluukomentoa. - Lopuksi asetetaan runtime-ympäristö, kuten GC ja riittävä pinomuisti, kutsumalla
runtime.morestack_noctxt.abi0(SB). - Siirrytään hallittuun
main.main(SB)-osoitteeseen.
Kuten nähdään, liiketoimintalogiikan assembly on melko selkeää, ja siihen on lisätty vain kevyt runtime-hallinta.
Ilman optimointia
Yllä oleva muoto on seurausta siitä, että Go-kääntäjä on automaattisesti inlinannut ja optimoinut kaksi erillistä funktiota. Oppimistarkoituksessa estämme tässä tapauksessa sayHello-funktion inlinauksen.
Tämä tehdään kääntämällä lähdekoodi seuraavalla lipulla:
1 go build -gcflags="-l" main.go
Tuloksia tarkasteltaessa huomataan päällekkäistä assembly-koodia.
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)
27yjlee@elegant:~/compare-assembly/go$ go tool objdump -s "main\.sayHello" ./main
28TEXT main.sayHello(SB) /home/yjlee/compare-assembly/go/main.go
29 main.go:3 0x468f60 493b6610 CMPQ SP, 0x10(R14)
30 main.go:3 0x468f64 7636 JBE 0x468f9c
31 main.go:3 0x468f66 55 PUSHQ BP
32 main.go:3 0x468f67 4889e5 MOVQ SP, BP
33 main.go:3 0x468f6a 4883ec10 SUBQ $0x10, SP
34 main.go:5 0x468f6e 4889442420 MOVQ AX, 0x20(SP)
35 main.go:5 0x468f73 48895c2428 MOVQ BX, 0x28(SP)
36 main.go:4 0x468f78 e8c3a3fcff CALL runtime.printlock(SB)
37 main.go:4 0x468f7d 488b442420 MOVQ 0x20(SP), AX
38 main.go:4 0x468f82 488b5c2428 MOVQ 0x28(SP), BX
39OVQ 0x20(SP), AX
40 main.go:4 0x468f82 488b5c2428 MOVQ 0x28(SP), BX
41 main.go:4 0x468f87 e834acfcff CALL runtime.printstring(SB)
42 main.go:4 0x468f8c e8efa5fcff CALL runtime.printnl(SB)
43 main.go:4 0x468f91 e80aa4fcff CALL runtime.printunlock(SB)
44 main.go:5 0x468f96 4883c410 ADDQ $0x10, SP
45 main.go:5 0x468f9a 5d POPQ BP
46 main.go:5 0x468f9b c3 RET
47 main.go:3 0x468f9c 4889442408 MOVQ AX, 0x8(SP)
48 main.go:3 0x468fa1 48895c2410 MOVQ BX, 0x10(SP)
49 main.go:3 0x468fa6 e8d5afffff CALL runtime.morestack_noctxt.abi0(SB)
50 main.go:3 0x468fab 488b442408 MOVQ 0x8(SP), AX
51 main.go:3 0x468fb0 488b5c2410 MOVQ 0x10(SP), BX
52 main.go:3 0x468fb5 eba9 JMP main.sayHello(SB)
53yjlee@elegant:~/compare-assembly/go$
Tästä vahvistuu, että kääntäjän tekemät optimoinnit kohdistuvat juuri tällaisiin päällekkäisiin operaatioihin ja tehottomaan loop unrollingiin.
Seuraavalla kerralla
Käsittelemme seuraavalla kerralla Go-kielen if- ja switch-lauseita. Jos aikaa riittää, tulemme analysoimaan myös Go-kielen runtime-osioita.