GoSuda

Какво представлява Managed Language?

By Lee Yunjin
views ...

Какво представлява управляваният (managed) език?

За разлика от неуправляваните езици, при които се изпълнява единствено логиката, съставена от програмиста, управляваните езици са такива, които изпълняват GC, runtime оптимизации, green threads, обработка на конкурентност и други по време на изпълнение, като по този начин освобождават потребителя от необходимостта да извършва рисковано управление на ниско ниво. При такива езици съществува предимството, че разработчикът може да се съсредоточи изцяло върху бизнес логиката, но от друга страна, е възможно програмата да се държи по начин, различен от интуицията на програмиста, което понякога налага прецизна настройка на runtime средата. Първо, ще разгледаме езика Go, който сред управляваните езици е най-верен на философията за минимализъм и чийто асемблер е най-прозрачен.

Бинарна структура на езика Go

.text.data.gopclntab, .typelink и др.
Машинен код за изпълнениеДанни за съхранениеСекции на runtime средата на езика
Тъй като езикът Go не превежда машинния код в съотношение 1:1 спрямо въведеното от потребителя, логиката в секцията .text е тясно свързана със секциите на runtime средата на езика.
Освен това, функции като runtime.printnl(), които не са написани от потребителя, се добавят към асемблерния код в секцията .text.
Чрез това автоматично вмъкване на код, езикът Go помага на разработчика да се освободи от ръчното управление.

Разглеждане само на функцията main в Go

Първо, нека напишем примерен изходен код main.go и да го разгледаме от main нататък върху AMD64 машина.

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

След това го компилираме по следния начин.

1go build main.go
2

Go поддържа go tool за улесняване на нискостепенното дебъгване. За да видим само асемблерния код на основната функция в основния пакет чрез go tool, въвеждаме следната команда.

1go tool objdump -s "main\.main" ./main
2

Асемблер

 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
  • Чрез CMPQ се сравнява текущият указател на стека (SP) със стойността на стековата защита в регистъра на контролния блок на горутината (R14), за да се провери дали пространството в стековата рамка на текущата горутина е достатъчно; ако не е, се извършва скок (JBE) към адреса 0x468f95, който е входна точка за разширяване на стека.
  • Предишният базов указател се записва в стека чрез PUSHQ BP.
  • Текущият указател на стека (SP) се копира в регистъра на базовия указател (BP), за да се фиксира отправната точка на стека в началото на функцията.
  • След това се заделя стеково пространство за локални променливи от 16 байта (SUBQ $0x10, SP) и се извършва подравняване на CPU кеша чрез попълване с виртуални инструкции с помощта на NOPL.
  • За синхронизация на стандартния изход за низове в Go runtime се извиква runtime.printlock(SB), за да се постави заключване.
  • С помощта на инструкцията LEAQ, началният адрес на константно зададения низ ("Hello World") се записва в регистъра AX, който според спецификацията Go ABI се използва за първия параметър сред регистрите с общо предназначение.
  • След това стойността, представляваща дължината на низа, се записва във втория параметър – регистъра BX (MOVL $0xb, BX, т.е. 11 в десетична бройна система).
  • Извиква се runtime.printstring(SB), за да се изведе информацията на конзолата въз основа на предадените AX (адрес на данни) и BX (дължина).
  • Извиква се runtime.printnl(SB) за обработка на нов ред.
  • Тъй като извеждането е завършено, заключването се освобождава чрез runtime.printunlock(SB).
  • Заделената 16-байтова стекова памет се освобождава чрез ADDQ $0x10, SP.
  • Предишният базов указател се възстановява чрез POPQ BP.
  • Чрез RET контролът се връща към точката, от която е извикана функцията.
  • Ако при първоначалната проверка на стека пространството е било недостатъчно, се извиква runtime.morestack_noctxt.abi0(SB) на адрес 0x468f95, за да се разшири динамично стековата runtime среда, както подобава на управляван език.
  • След приключване на разширяването на стека, програмата се връща (JMP) към входната точка на main.main(SB). Както се вижда, асемблерният код на бизнес логиката е доста ясен и към него е добавено само леко runtime управление.

При липса на оптимизация

Горната форма е резултат от автоматично инлайн оптимизиране на две отделни функции от страна на компилатора на Go. Въпреки това, за целите на обучението, в този случай ще предотвратим инлайнването на sayHello. За да направим това, компилираме изходния код със следния флаг.

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

Ако изведем резултата в обвивката (shell), ще открием дублиращ се асемблерен код.

 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

Когато деактивираме инлайнването, се вмъкват MOVQ операции, които записват стойностите обратно в стековия указател с отместване, като 0x20(SP), за да се запазят параметрите (AX, BX) според конвенцията за извикване на функции. С други думи, потвърдено е, че целта на оптимизацията от страна на компилатора е именно премахването на тези ненужни операции по преместване в паметта и извикването на функции (overhead).

Следващият път

Следващия път ще разгледаме if и switch конструкциите в езика Go. Ако времето позволи, в бъдеще планирам да анализирам и секциите на runtime средата на Go.