Qu'est-ce qu'un Managed Language ?
Qu'est-ce qu'un langage managé ?
Un langage managé, contrairement à un langage non managé — c'est-à-dire un langage qui s'exécute sans s'écarter significativement de la logique établie par le programmeur — est un langage qui exécute au sein du runtime des fonctionnalités telles que le GC, l'optimisation du runtime, les green threads et la gestion de la concurrence, dispensant ainsi l'utilisateur de procéder à une gestion périlleuse de bas niveau.
Dans le cas de ces langages, bien que l'avantage réside dans la possibilité de se concentrer exclusivement sur la logique métier, il arrive que le programme puisse se comporter différemment de l'intuition du programmeur, nécessitant parfois un réglage fin du runtime.
Pour commencer, nous allons examiner le langage Go, qui, parmi les langages managés, est le plus fidèle à la philosophie minimaliste et dont l'assembly est le plus explicite.
Structure binaire du langage Go
| .text | .data | .gopclntab, .typelink etc. |
|---|---|---|
| Code machine à exécuter | Données à stocker | Sections du runtime du langage |
Étant donné que le langage Go n'effectue pas une traduction machine 1:1 fidèle à l'entrée de l'utilisateur, la logique de la section .text est étroitement liée aux sections du runtime du langage.
De plus, des fonctions que l'utilisateur n'a pas explicitement écrites, telles que runtime.printnl(), sont ajoutées à l'assembly de la section .text.
Grâce à cette insertion automatique de code, le langage Go aide le développeur à s'affranchir d'une gestion manuelle.
Observation de la fonction main en Go
Tout d'abord, rédigeons un exemple de code source simple, main.go, et examinons-le depuis main sur une machine AMD64.
1package main
2
3func sayHello(msg string) {
4 println(msg)
5}
6
7func main() {
8 sayHello("Hello World")
9}
Ensuite, nous procédons à la compilation comme suit :
1go build main.go
Go prend en charge go tool pour faciliter le débogage de bas niveau.
Pour n'afficher que l'assembly correspondant à la fonction main du package main dans go tool, nous saisissons cette commande :
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)
- Après avoir comparé par
CMPQsi le thread actuel est entré, si c'est le cas, il saute au point d'entrée 0x468f95. - Le point d'entrée est inséré dans la pile avec
PUSHQ BP. - Le point de départ de la pile est spécifié au début de la fonction dans le registre SP, où les données ont été chargées le plus récemment, afin de fixer le point d'entrée pour la référence aux variables locales.
- Ensuite, une pile de 16 octets est réservée pour les variables locales (
SUBQ $0x10, SP), etNOPLest utilisé pour remplir plusieurs octets afin d'aligner le cache CPU. - Le runtime Go verrouille la sortie du tampon de chaîne en appelant
runtime.printlock(SB). - L'instruction
LEAQest utilisée pour stocker l'adresse de début de la chaîne allouée dans AX, l'accumulateur utilisé pour le stockage des données parmi les registres à usage général. - Ensuite, la longueur de la chaîne, 11, est stockée dans le registre BX, utilisé pour l'assistance aux calculs et le stockage temporaire de données. (
MOVL $0Xb, BX) - Les informations de l'accumulateur sont envoyées vers SB via
runtime.printstring(SB). - Un saut de ligne est également écrit vers SB via
runtime.printnl(SB). - Le tampon de chaîne est libéré par
runtime.printunlock(SB). ADDQ $0x10, SPrestitue les 16 octets de mémoire de pile empruntés. - Comme le point d'entrée avait été initialement placé sur la pile, on le retire avecPOPQ BPavant d'envoyer le signal de retour.- Ensuite,
runtime.morestack_noctxt.abi0(SB)alloue une pile suffisante, comme il sied à un langage managé, et configure le runtime tel que le GC. - Le programme se déplace vers l'adresse gérée
main.main(SB).
Comme on peut le constater, l'assembly de la logique métier est assez clair, avec seulement une fine couche de gestion du runtime ajoutée.
En l'absence d'optimisation
La forme ci-dessus est le résultat de l'optimisation automatique par le compilateur Go, qui a effectué l'inlining des deux fonctions initialement séparées. Cependant, pour les besoins de notre apprentissage, nous allons éviter l'inlining de sayHello dans ce cas précis.
Pour ce faire, nous compilons la source avec le flag suivant :
1 go build -gcflags="-l" main.go
En affichant les résultats dans le shell, on découvre un assembly redondant.
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$
En somme, il est confirmé que ce que le compilateur optimise, ce sont précisément ces opérations redondantes, le déroulement inefficace des boucles, etc.
La prochaine fois
La prochaine fois, nous aborderons les instructions if et switch dans le langage Go. Si le temps le permet, nous analyserons également les sections du runtime Go ultérieurement.