GoSuda

Mi a managed language?

By Lee Yunjin
views ...

Mi az a managed nyelv?

A managed nyelv az unmanaged nyelvektől – azaz azoktól a nyelvektől, amelyek a programozó által írt logikát nem követik érdemben, csupán végrehajtják azt – eltérően olyan funkciókat biztosít futásidőben, mint a GC, a futásidejű optimalizáció, a green threads és a konkurens feldolgozás, így a felhasználónak nem kell kockázatos, alacsony szintű memóriakezeléssel foglalkoznia. Az ilyen nyelvek esetében előny, hogy a fejlesztő kizárólag az üzleti logikára koncentrálhat, ugyanakkor hátrány, hogy a program a programozói intuíciótól eltérően működhet, ami esetenként kifinomult futásidejű hangolást tesz szükségessé. Először a managed nyelvek közül a minimalista filozófiát leginkább követő, az assembly szinten is transzparens Go nyelvet vizsgáljuk meg.

A Go nyelv bináris struktúrája

.text.data.gopclntab, .typelink stb.
Végrehajtandó gépi kódTárolandó adatokNyelvi futásidejű szekció
Mivel a Go nyelv nem 1:1 arányban fordítja le a felhasználói kódot gépi kódra, a .text szekció logikája szorosan összefonódik a nyelvi futásidejű szekciókkal.
Ezenkívül olyan, a felhasználó által nem definiált függvények, mint a runtime.printnl(), szintén bekerülnek a .text szekció assembly kódjába.
Ezen automatikus kódbeillesztések révén a Go nyelv segít a fejlesztőnek megszabadulni a manuális kezelés terhétől.

A main függvény vizsgálata Go nyelven

Először is írjunk egy egyszerű main.go forrásfájlt, és tekintsük meg a main függvényt AMD64 architektúrán.

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

Ezt követően fordítsuk le az alábbi módon:

1go build main.go
2

A Go támogatja a go tool eszközt az egyszerű, alacsony szintű hibakeresés érdekében. A fő csomagban lévő main függvényhez tartozó assembly kód megjelenítéséhez az alábbi parancsot használjuk:

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
  • A program a CMPQ utasítással összehasonlítja a veremmutatót (SP) a gorutin vezérlőblokk regiszterében (R14) tárolt veremvédelmi értékkel, és ha a veremterület nem elegendő, a 0x468f95 belépési címre ugrik (JBE) a verem bővítéséhez.
  • Az előző bázismutatót a PUSHQ BP utasítással a verembe menti.
  • A bázismutató (BP) regiszterbe másolja az aktuális veremmutatót (SP), rögzítve ezzel a verem kezdőpontját a függvény elején.
  • Ezt követően 16 bájtnyi helyet foglal a lokális változók számára (SUBQ $0x10, SP), majd a NOPL utasítással virtuális parancsokat tölt be a CPU-gyorsítótár igazítása érdekében.
  • A Go futáskörnyezetben a belső sztringek standard kimenetre történő szinkronizálása érdekében meghívja a runtime.printlock(SB) függvényt a zároláshoz.
  • A LEAQ utasítással a konstansként tárolt sztring ("Hello World") kezdőcímét a Go ABI szabvány szerinti első paraméter regiszterbe, az AX-be tölti.
  • Ezt követően a sztring hosszát a második paraméter regiszterbe, a BX-be tölti (MOVL $0xb, BX, azaz decimálisan 11).
  • Meghívja a runtime.printstring(SB) függvényt, amely az AX (adatcím) és BX (hossz) információk alapján kiírja az adatokat a konzolra.
  • A soremeléshez meghívja a runtime.printnl(SB) függvényt.
  • A kiírás befejeztével a runtime.printunlock(SB) hívásával feloldja a zárolást.
  • Az ADDQ $0x10, SP utasítással felszabadítja a korábban lefoglalt 16 bájtos veremterületet.
  • A POPQ BP utasítással visszaállítja az előző bázismutatót.
  • A RET utasítással visszaadja a vezérlést a hívó függvénynek.
  • Amennyiben a kezdeti veremellenőrzés során nem volt elegendő hely, a program meghívja a 0x468f95 címen található runtime.morestack_noctxt.abi0(SB) függvényt, amely a managed nyelvek sajátosságának megfelelően dinamikusan bővíti a veremterületet.
  • A verem bővítése után a program visszatér (JMP) a main.main(SB) belépési pontjára. Amint látható, az üzleti logika assembly kódja meglehetősen tiszta, és csak minimális futásidejű menedzsmenttel egészül ki.

Optimalizáció hiánya

A fenti forma annak az eredménye, hogy a Go fordító automatikusan inlining technológiát alkalmazott a két különálló függvényre. Tanulási célból azonban ebben az esetben tiltsuk le a sayHello függvény inliningját. Ehhez a következő jelölővel (flag) fordítsuk a forráskódot:

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

A parancssori kimenetben ellenőrizve redundáns assembly kód figyelhető meg.

 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

Az inlining kikapcsolásakor a függvényhívási konvenciónak megfelelően a paraméterek (AX, BX) megőrzése érdekében a veremmutató eltolásával (pl. 0x20(SP)) MOVQ műveletek kerülnek beillesztésre. Ebből látható, hogy a fordító optimalizációja többek között az ilyen felesleges memóriamozgatási műveletek és a hívási többletköltségek (overhead) kiküszöbölésére irányul.

Következő alkalommal

A következő részben a Go nyelv if és switch utasításaival fogunk foglalkozni. Amennyiben időnk engedi, a későbbiekben a Go futásidejű szekcióinak elemzésére is sort kerítünk.