GoSuda

Czym jest język managed?

By Lee Yunjin
views ...

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 maszynowyPrzechowywane daneSekcje 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.