Eliminação de chamadas de cauda no iOS
A otimização da chamada final, chamada reutilizando a pilha do chamador, é suportada atualmente em x86/x86–64, PowerPC e WebAssembly. É executado em x86/x86–64 e PowerPC — lldb
Conhecimento básico
Registros
As operações do processador envolvem principalmente o processamento de dados. Esses dados podem ser armazenados na memória e acessados a partir dela. No entanto, ler dados e armazenar dados na memória torna o processador mais lento, pois envolve processos complicados de enviar a solicitação de dados pelo barramento de controle e the memory storage unit
obter os dados por meio do mesmo canal. Para acelerar as operações do processador, o processador inclui alguns registros internal memory storage locations
, chamados registradores .
Os registradores armazenam elementos de dados para processamento sem ter que acessar a memória. Um número limitado de registradores é incorporado ao chip do processador. — https://www.tutorialspoint.com/assembly_programming/assembly_registers.htm
No ARM 64, o gráfico a seguir mostra as funções dos registradores.

- Os oito primeiros registradores, r0-r7, são usados para passar valores de argumento para uma sub-rotina e para retornar valores de resultado de uma função.
- O registro de quadro para o quadro mais interno (pertencente à chamada de rotina mais recente) deve ser apontado pelo
Frame Pointer register
(FP). A palavra dupla endereçada mais baixa deve apontar paraprevious frame record
e a palavra dupla endereçada mais alta deve conter o valor passado em LR na entrada da função atual.
A pilha é uma contiguous area of memory
que pode ser usada para armazenamento de variáveis locais e para passar argumentos adicionais para sub-rotinas quando não houver registradores de argumento suficientes disponíveis. A implementação da pilha é totalmente descendente , the current extent of the stack
mantida no registrador de propósito especial SP
. - Padrão de Chamada de Procedimento para a Arquitetura ARM de 64 bits (AArch64) - AArch64 ABI release 1.0
O ambiente ARM usa uma pilha que, no ponto das chamadas de função, cresce para baixo e contém variáveis locais e os parâmetros de uma função. A pilha é alinhada no ponto de chamadas de função. A Figura 1 mostra a pilha antes e durante uma chamada de sub-rotina.

Os quadros de pilha contêm as seguintes áreas:
- A área de parâmetros armazena os argumentos que o chamador passa para a função chamada ou armazena espaço para eles, dependendo do tipo de cada argumento e da disponibilidade de registradores. Essa área reside no quadro de pilha do chamador.
- A área de ligação contém o endereço da próxima instrução do chamador.
- O ponteiro de quadro salvo (opcional) contém o endereço base do quadro de pilha do chamador.
- A área de armazenamento local contém as variáveis locais da sub-rotina e os valores dos registradores que devem ser restaurados antes do retorno da função chamada. Consulte Preservação de registros para obter detalhes.
- A área de registros salvos contém os valores dos registros que devem ser restaurados antes que a função chamada retorne. Consulte Preservação de registros para obter detalhes.
Outro gráfico de layout de quadro de pilha vem do padrão de chamada de procedimento para a arquitetura ARM de 64 bits (AArch64) - AArch64 ABI versão 1.0

Passando Argumentos
Quando as funções (rotinas) chamam outras funções (sub-rotinas), elas podem precisar passar argumentos para elas. As sub-rotinas chamadas acessam esses argumentos como parâmetros . Por outro lado, algumas sub-rotinas passam um resultado ou retornam um valor para seus chamadores. No ambiente ARMv6, os argumentos podem ser passados na pilha de tempo de execução ou em registradores; além disso, alguns argumentos vetoriais também são passados em registradores. Os resultados são retornados em registradores ou na memória. Para passar valores de forma eficiente entre chamadores e chamados, o GCC segue regras estritas ao gerar o código de objeto de um programa. — ARMv6
O padrão básico permite a passagem de argumentos em registradores de uso geral (r0-r7)
Retornando resultados
A maneira como um resultado é retornado de uma função é determinada pelo tipo desse resultado:
- Se o tipo,
T,
do resultado de uma função é tal que void func(T arg)
exigiria quearg
fosse passado como um valor em um registrador, então o resultado seria retornado nos mesmos registradores que seriam usados para tal argumento.- Caso contrário,
the caller shall reserve a block of memory of sufficient size and alignment to hold the result
. O endereço do bloco de memória deve ser passado como um argumento adicional para a função emx8
.The callee may modify the result memory block
em qualquer ponto durante a execução da sub-rotina (não há nenhuma exigência para o callee preservar o valor armazenado em x8).
Caso normal
Digamos que temos um método drawRect
, que chamaCGContextDrawPath
- (void) drawRect: (NSRect) rect {
// draw stuff
…
CGContextDrawPath(_path, kCGStroke);
}
wwdc
Aqui, fp
está o ponteiro do quadro . lr
é o link register
, que contém a próxima instrução a ser executada na função chamadora (rotinas) que chamou a função atual (sub-rotinas). Portanto, a função atual, subroutines
saiba como retornar ao seu chamador.
Quando o UIKit chama drawRect
, primeiramente, drawRect
estabelecerá seu frame de chamada empurrando o return
endereço do link registe( lr
) e previous value
do frame pointer ( frame Ptr
), na pilha. Como dissemos antes, existem registradores que armazenam esses dois dados.
+0x00 push {r4, r5, r6, fp, lr}

Em seguida, drawRect
abre espaço para armazenar variáveis locais e tem seu frame de chamada na pilha.
+0x04 sub sp, #180

A maneira como o criador de perfil de tempo funciona, ele usa um serviço no kernel que fará uma amostra do que as CPUs estão fazendo em cerca de 1000x por segundo. Nesse caso, se pegarmos uma amostra, veremos que estamos executando dentro do caminho de desenho de contexto.
Em seguida, o kernal olha para o registrador do ponteiro do quadro para ver onde está a base do quadro dessa função e encontrar o endereço de retorno de quem o chamou.
Rastreamento
Então, o que é Backtrace? Bem, usando o ponteiro do quadro que foi colocado na pilha, sabemos o endereço base do quadro da pilha do chamador. Como cada quadro de chamada do método armazena o endereço base do quadro de pilha de seu chamador, podemos usar isso para percorrer a cadeia de quadros de chamada nessa pilha.

Caixa otimizada
Em drawRect
, depois de chamar CGContextDrawPath
, vai para return
, o que significa que na verdade vai pop stack frame
, restore fp
e então jump back to caller
.

Como CGContextDrawPath
não precisa de nada de drawRect
, os parâmetros de que precisa não são do quadro de chamada de drawRect
, o compilador faz uma otimização aqui. Ele reorganiza o código da seguinte maneira. Eu pops stack frame
de drawRect
primeiro, restores fp
, então chama CGContextDrawPath
. Então não precisa jump back to caller
, o endereço armazenado no lr
cadastro.
Figura 1. vaipops stack frame

Figura 2. depois de remover o quadro de pilha de drawRect
, o quadro de chamada de drawRect não existe mais na pilha. Então sp
se move.

Figura 3. depois de restaurar fp
para o endereço base do ponteiro do quadro do chamador, o return address
e frame ptr
de drawRect
não existe mais na pilha. Então sp
se move.

Por fim, ele chama CGContextDrawPath
, que estabelece seu próprio quadro de chamada na Pilha. Então, a pilha está assim agora.

A diferença na Árvore de Chamadas
O lado esquerdo é o caso normal, o lado direito é o caso otimizado onde o frame de chamada drawRect
será removido da pilha no caso otimizado. Parece que CGContextDrawPath
é chamado diretamente de drawLayout:inContext
.

O benefício da eliminação da chamada de cauda
- Economiza memória de pilha
- Mantém os caches quentes e reutiliza a memória
- Melhor para chamadas de cauda de função recursivas, especialmente
tail call recursive code
, onde uma função ou método chama a si mesmo como a última linha de código e depois retorna. Com o Tail call Elimination, a pilha não crescerá tanto a ponto de conseguirmos altas performances.
definir este sinalizador de compilador ao compilar, você pode obter uma árvore de chamadas melhor no Time Profiler ao criar o perfil do aplicativo.
CFLAGS=“-fno-optimize-sibling-calls”
Existe um truque útil para identificar se este é um caso de eliminação de chamada de cauda. Isso é paralook at the disaeembly and call sight of the last call

- Em chamada normal, usa
a Branch and Link family of instructions
(bl); Definelr
o próximo endereço e salta para o chamador
Caller: +0x174 blx "CGContextDrawPath $shim”
Caller: +0x174 b.w "CGContextDrawPath $shim”