Czym jest język managed?
Czym jest język zarządzany (managed language)?
Język zarządzany to język, który w odróżnieniu od języka niezarządzanego – czyli takiego, który wykonuje jedynie logikę napisaną przez programistę bez znaczącej ingerencji – realizuje w czasie wykonywania (runtime) funkcje takie jak GC, optymalizacja runtime, green threads czy obsługa współbieżności, uwalniając tym samym użytkownika od konieczności wykonywania ryzykownych operacji niskopoziomowych.
W przypadku takich języków zaletą jest możliwość skupienia się wyłącznie na logice biznesowej, co sprzyja koncentracji na procesie programowania. Z drugiej strony, ponieważ program może działać inaczej, niż sugerowałaby to intuicja programisty, niekiedy niezbędna staje się precyzyjna optymalizacja runtime.
Na początek przyjrzymy się językowi Go, który spośród języków zarządzanych najwierniej realizuje filozofię minimalistyczną i charakteryzuje się najbardziej przejrzystym kodem asemblerowym.
Struktura binarna języka Go
| .text | .data | .gopclntab, .typelink itp. |
|---|---|---|
| Wykonywalny kod maszynowy | Przechowywane dane | Sekcje runtime języka |
Ponieważ język Go nie dokonuje translacji na kod maszynowy w stosunku 1:1 względem danych wejściowych użytkownika, logika w sekcji .text jest ściśle powiązana z sekcjami runtime języka.
Ponadto w asemblerze sekcji .text umieszczane są funkcje, takie jak runtime.printnl(), których użytkownik nie napisał samodzielnie. Dzięki tak automatycznemu wstrzykiwaniu kodu język Go pomaga programiście uniknąć ręcznego zarządzania zasobami.
Analiza funkcji main w Go
W pierwszej kolejności stwórzmy prosty przykład źródłowy main.go i przeanalizujmy funkcję main na maszynie AMD64.
1package main
2
3func sayHello(msg string) {
4 println(msg)
5}
6
7func main() {
8 sayHello("Hello World")
9}
Następnie kompilujemy go w następujący sposób:
1go build main.go
Go wspiera go tool, co ułatwia debugowanie niskopoziomowe.
Aby wyświetlić asembler wyłącznie dla głównej funkcji w głównym pakiecie za pomocą go tool, wprowadzamy następującą komendę:
1go tool objdump -s "main\.main" ./main
Asembler
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)
- Po porównaniu za pomocą CMPQ, czy bieżący wątek został zainicjowany, w przypadku powodzenia następuje skok do punktu wejścia 0x468f95.
- Punkt wejścia jest wstawiany do stosu za pomocą
PUSHQ BP. - Rejestr SP, w którym ostatnio umieszczono dane, wskazuje początek stosu na początku funkcji, co ustala punkt wejścia przy odwołaniach do zmiennych lokalnych.
- Następnie rezerwowane jest 16 bajtów na stos zmiennych lokalnych (
SUBQ $0x10, SP), a NOPL jest używany do wypełnienia bajtów w celu wyrównania cache procesora. - Runtime Go blokuje wyjście bufora stringa poprzez wywołanie
runtime.printlock(SB). - Instrukcja LEAQ zapisuje adres początkowy przydzielonego ciągu znaków w rejestrze AX, który jest akumulatorem używanym do przechowywania danych.
- Następnie w rejestrze BX, używanym do operacji pomocniczych i tymczasowego przechowywania danych, zapisywana jest długość ciągu znaków równa 11. (
MOVL $0xb, BX) - Informacje z akumulatora są wyprowadzane do SB za pomocą runtime.printstring(SB).
- Pusta linia jest również zapisywana do SB za pomocą runtime.printnl(SB).
- Bufor stringa jest zwalniany za pomocą runtime.printunlock(SB).
- Pamięć stosu o rozmiarze 16 bajtów jest zwalniana za pomocą ADDQ $0x10, SP. Ponieważ punkt wejścia został na początku umieszczony na stosie, teraz za pomocą POPQ BP jest on usuwany ze stosu, po czym wysyłany jest sygnał powrotu.
- Następnie wywoływane jest runtime.morestack_noctxt.abi0(SB), co – zgodnie z naturą języka zarządzanego – przydziela wystarczającą ilość stosu i konfiguruje runtime, w tym GC.
- Następuje przejście do adresu zarządzanego main.main(SB).
Jak widać, asembler logiki biznesowej jest dość przejrzysty i zawiera jedynie dodatek w postaci lekkiego zarządzania przez runtime.
W przypadku braku optymalizacji
Powyższa postać jest wynikiem automatycznego inliningu dwóch odrębnych funkcji przez kompilator Go. Jednakże, w celach edukacyjnych, w tym przypadku nie będziemy stosować inliningu dla sayHello.
W tym celu kompilujemy źródło z użyciem następującej flagi:
1 go build -gcflags="-l" main.go
Wyświetlając wynik w powłoce, można zauważyć powielony kod asemblerowy.
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$
Potwierdzono zatem, że kompilator optymalizuje m.in. operacje powielone oraz nieefektywne rozwijanie pętli (loop unrolling).
Następnym razem
W kolejnej części omówimy instrukcje warunkowe if oraz switch w języku Go. Jeżeli czas pozwoli, w przyszłości zajmiemy się również analizą sekcji runtime języka Go.