Em muitos casos, a maneira ideal de executar alguma tarefa pode depender do contexto em que a tarefa é executada. Se uma rotina é escrita em linguagem assembly, geralmente não será possível que a sequência de instruções varie com base no contexto. Como um exemplo simples, considere o seguinte método simples:
inline void set_port_high(void)
{
(*((volatile unsigned char*)0x40001204) = 0xFF);
}
Um compilador para código ARM de 32 bits, dado o exposto acima, provavelmente o renderizará como algo como:
ldr r0,=0x40001204
mov r1,#0
strb r1,[r0]
[a fourth word somewhere holding the constant 0x40001204]
ou talvez
ldr r0,=0x40001000 ; Some assemblers like to round pointer loads to multiples of 4096
mov r1,#0
strb r1,[r0+0x204]
[a fourth word somewhere holding the constant 0x40001000]
Isso pode ser otimizado levemente no código montado à mão, como:
ldr r0,=0x400011FF
strb r0,[r0+5]
[a third word somewhere holding the constant 0x400011FF]
ou
mvn r0,#0xC0 ; Load with 0x3FFFFFFF
add r0,r0,#0x1200 ; Add 0x1200, yielding 0x400011FF
strb r0,[r0+5]
Ambas as abordagens montadas manualmente exigiriam 12 bytes de espaço de código em vez de 16; o último substituiria uma "carga" por uma "adição", que executaria em um ARM7-TDMI dois ciclos mais rápidos. Se o código fosse executado em um contexto em que r0 não sabia / não se importa, as versões da linguagem assembly seriam assim um pouco melhores que a versão compilada. Por outro lado, suponha que o compilador sabia que algum registro [por exemplo, r5] manteria um valor dentro de 2047 bytes do endereço desejado 0x40001204 [por exemplo 0x40001000] e ainda sabia que outro registro [por exemplo, r7] para manter um valor cujos bits baixos fossem 0xFF. Nesse caso, um compilador pode otimizar a versão C do código para simplesmente:
strb r7,[r5+0x204]
Muito mais curto e rápido do que o código de montagem otimizado manualmente. Além disso, suponha que set_port_high ocorreu no contexto:
int temp = function1();
set_port_high();
function2(temp); // Assume temp is not used after this
Não é de todo implausível ao codificar para um sistema incorporado. Se set_port_high
estiver escrito no código do assembly, o compilador precisaria mover r0 (que retém o valor de retorno function1
) para outro lugar antes de chamar o código do assembly e, em seguida, mover esse valor de volta para r0 posteriormente (pois function2
esperará seu primeiro parâmetro em r0), portanto, o código de montagem "otimizado" precisaria de cinco instruções. Mesmo que o compilador não soubesse de nenhum registro contendo o endereço ou o valor a ser armazenado, sua versão de quatro instruções (que poderia ser adaptada para usar os registros disponíveis - não necessariamente r0 e r1) venceria o assembly "otimizado" versão em vários idiomas. Se o compilador tiver o endereço e os dados necessários em r5 e r7, conforme descrito anteriormente, com uma única instrução -function1
não alteraria esses registros e, portanto, poderia substituirset_port_high
strb
quatro instruções menores e mais rápidas que o código de montagem "otimizado manualmente".
Observe que o código de montagem otimizado manualmente pode superar um compilador nos casos em que o programador conhece o fluxo preciso do programa, mas os compiladores brilham nos casos em que um trecho de código é escrito antes de seu contexto ser conhecido ou onde um trecho de código-fonte pode ser invocado de vários contextos [se set_port_high
for usado em cinquenta lugares diferentes no código, o compilador poderá decidir independentemente para cada um qual a melhor forma de expandi-lo].
Em geral, eu sugeriria que a linguagem assembly é capaz de fornecer as maiores melhorias de desempenho nos casos em que cada parte do código pode ser abordada a partir de um número muito limitado de contextos e é prejudicial ao desempenho em locais onde uma parte da código pode ser abordado de muitos contextos diferentes. Curiosamente (e convenientemente) os casos em que a montagem é mais benéfica para o desempenho são geralmente aqueles em que o código é mais direto e fácil de ler. Os locais em que o código da linguagem assembly se tornaria uma bagunça pegajosa são geralmente aqueles em que a escrita em assembly ofereceria o menor benefício de desempenho.
[Nota secundária: existem alguns lugares onde o código de montagem pode ser usado para gerar uma bagunça pegajosa hiper otimizada; por exemplo, um pedaço de código que fiz para o ARM precisava buscar uma palavra da RAM e executar uma das cerca de doze rotinas com base nos seis bits superiores do valor (muitos valores mapeados para a mesma rotina). Eu acho que otimizei esse código para algo como:
ldrh r0,[r1],#2! ; Fetch with post-increment
ldrb r1,[r8,r0 asr #10]
sub pc,r8,r1,asl #2
O registrador r8 sempre mantinha o endereço da tabela principal de despacho (dentro do loop em que o código gasta 98% de seu tempo, nada o utilizava para qualquer outro propósito); todas as 64 entradas se referiam a endereços nos 256 bytes anteriores a ela. Como o loop primário tinha, na maioria dos casos, um limite de tempo de execução rígido de cerca de 60 ciclos, a busca e o despacho de nove ciclos foram muito úteis para atingir esse objetivo. O uso de uma tabela de 256 endereços de 32 bits seria um ciclo mais rápido, mas consumiria 1 KB de RAM muito preciosa [o flash teria adicionado mais de um estado de espera]. Usar 64 endereços de 32 bits exigiria adicionar uma instrução para mascarar alguns bits da palavra buscada e ainda assim devoraria 192 bytes a mais do que a tabela que realmente usei. O uso da tabela de compensações de 8 bits produziu código muito compacto e rápido, mas não é algo que eu esperaria que um compilador pudesse inventar; Eu também não esperaria que um compilador dedicasse um registro "em tempo integral" para manter o endereço da tabela.
O código acima foi projetado para funcionar como um sistema independente; poderia chamar periodicamente o código C, mas somente em determinados momentos em que o hardware com o qual estava se comunicando podia ser colocado com segurança em um estado "inativo" por dois intervalos de aproximadamente um milissegundo a cada 16ms.