O que é uma managed language?
O que é uma linguagem gerenciada?
Uma linguagem gerenciada é aquela que, diferentemente das linguagens não gerenciadas — ou seja, linguagens que apenas executam a lógica estruturada pelo programador sem grandes abstrações —, executa o GC, otimizações de runtime, green threads, processamento de concorrência e outros recursos em tempo de execução, dispensando o usuário da necessidade de realizar um gerenciamento de baixo nível perigoso.
No caso dessas linguagens, existe a vantagem de poder concentrar-se exclusivamente na lógica de negócio e na imersão no desenvolvimento; por outro lado, como o comportamento real do programa pode divergir da intuição do programador, por vezes torna-se necessário um ajuste refinado de runtime.
Primeiramente, analisaremos a linguagem Go, que é a que mais se mantém fiel à filosofia minimalista entre as linguagens gerenciadas e possui um assembly transparente.
Estrutura binária da linguagem Go
| .text | .data | .gopclntab, .typelink etc. |
|---|---|---|
| Código de máquina a ser executado | Dados a serem armazenados | Seções de runtime da linguagem |
Como a linguagem Go não realiza a tradução para código de máquina de forma 1:1 conforme a entrada do usuário, a lógica na seção .text está intimamente relacionada com as seções de runtime da linguagem.
Além disso, funções como runtime.printnl(), que não foram escritas pelo usuário, são adicionadas ao assembly da seção .text.
Por meio dessa inserção automática de código, a linguagem Go ajuda a liberar o desenvolvedor do gerenciamento manual.
Visualizando apenas a função main em Go
Inicialmente, vamos elaborar um exemplo simples de código-fonte main.go e observar a partir da função main em uma máquina AMD64.
1package main
2
3func sayHello(msg string) {
4 println(msg)
5}
6
7func main() {
8 sayHello("Hello World")
9}
Em seguida, realizamos a compilação desta forma:
1go build main.go
Go oferece suporte ao go tool para facilitar a depuração de baixo nível.
Para visualizar apenas o assembly correspondente à função principal dentro do pacote principal no go tool, insira este comando:
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)
- Após comparar se a thread atual entrou com CMPQ, se for verdadeiro, ocorre um salto para o Entrypoint 0x468f95.
- O ponto de entrada é inserido na pilha com
PUSHQ BP. - O ponto de início da pilha no momento da função é especificado no registrador SP, onde os dados foram carregados mais recentemente, fixando o ponto de entrada para a referência de variáveis locais.
- Em seguida, reserva-se uma pilha de variáveis locais de 16 bytes (
SUBQ $0x10, SP) e utiliza-se NOPL para preencher vários bytes e alinhar o cache da CPU. - O runtime do Go bloqueia a saída do buffer de string chamando
runtime.printlock(SB). - Utiliza-se a instrução LEAQ para salvar o endereço inicial da string alocada no registrador AX, que é o acumulador utilizado para armazenamento de dados entre os registradores de propósito geral.
- Posteriormente, armazena-se o comprimento da string, 11, no registrador BX, usado como auxiliar de operação e para armazenamento temporário de dados. (
MOVL $0xb, BX) - As informações do acumulador são impressas para SB através de
runtime.printstring(SB). - Um espaço em branco de uma linha também é gravado em SB através de
runtime.printnl(SB). - O buffer de string é liberado com
runtime.printunlock(SB). - A memória de pilha de 16 bytes emprestada é devolvida com
ADDQ $0x10, SP. - Como o ponto de entrada foi inicialmente inserido na pilha para notificação, agora o ponto de entrada é removido da pilha comPOPQ BPe um sinal de retorno é emitido. - Em seguida, com
runtime.morestack_noctxt.abi0(SB), conforme esperado de uma linguagem gerenciada, aloca-se pilha suficiente e configura-se o runtime, incluindo o GC. - O processamento segue para o endereço gerenciado
main.main(SB).
Como observado, o assembly da lógica de negócio é bastante claro, apresentando apenas uma camada leve de gerenciamento de runtime.
Sem otimização
A forma acima é o resultado da otimização realizada pelo compilador Go, que automaticamente realizou o inlining de duas funções separadas. Contudo, para fins didáticos, não realizaremos o inlining de sayHello neste caso.
Para tanto, compilamos o código-fonte com a seguinte flag:
1 go build -gcflags="-l" main.go
Ao exibir os resultados no shell, descobre-se a presença de assembly duplicado.
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$
Ou seja, confirmou-se que o que o compilador otimiza são alvos como essas operações duplicadas, loop unrolling ineficiente, entre outros.
Próxima etapa
Na próxima vez, abordaremos as instruções if e switch na linguagem Go. Caso tenhamos tempo futuramente, também analisaremos as seções de runtime do Go.