In molti casi, il modo ottimale per eseguire alcune attività può dipendere dal contesto in cui l'attività viene eseguita. Se una routine è scritta nel linguaggio assembly, in genere non sarà possibile variare la sequenza delle istruzioni in base al contesto. Come semplice esempio, considera il seguente metodo semplice:
inline void set_port_high(void)
{
(*((volatile unsigned char*)0x40001204) = 0xFF);
}
Un compilatore per il codice ARM a 32 bit, dato quanto sopra, probabilmente lo renderebbe come qualcosa di simile:
ldr r0,=0x40001204
mov r1,#0
strb r1,[r0]
[a fourth word somewhere holding the constant 0x40001204]
o forse
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]
Ciò potrebbe essere leggermente ottimizzato nel codice assemblato a mano, in quanto:
ldr r0,=0x400011FF
strb r0,[r0+5]
[a third word somewhere holding the constant 0x400011FF]
o
mvn r0,#0xC0 ; Load with 0x3FFFFFFF
add r0,r0,#0x1200 ; Add 0x1200, yielding 0x400011FF
strb r0,[r0+5]
Entrambi gli approcci assemblati a mano richiederebbero 12 byte di spazio di codice anziché 16; quest'ultimo sostituirà un "carico" con un "add", che su un ARM7-TDMI eseguirà due cicli più velocemente. Se il codice fosse eseguito in un contesto in cui r0 non era noto / non importa, le versioni del linguaggio assembly sarebbero quindi leggermente migliori rispetto alla versione compilata. D'altra parte, supponiamo che il compilatore sapesse che alcuni registri [ad es. R5] avrebbero mantenuto un valore compreso tra 2047 byte dell'indirizzo desiderato 0x40001204 [ad es. 0x40001000], e inoltre sapevano che altri registri [ad es. R7] stavano andando per contenere un valore i cui bit bassi erano 0xFF. In tal caso, un compilatore potrebbe ottimizzare la versione C del codice semplicemente:
strb r7,[r5+0x204]
Molto più breve e veloce del codice assembly ottimizzato a mano. Supponiamo inoltre che set_port_high si sia verificato nel contesto:
int temp = function1();
set_port_high();
function2(temp); // Assume temp is not used after this
Non è affatto plausibile quando si codifica un sistema incorporato. Se set_port_high
è scritto nel codice assembly, il compilatore dovrebbe spostare r0 (che contiene il valore restituito function1
) da qualche altra parte prima di richiamare il codice assembly, quindi riportare quel valore in r0 in seguito (poiché function2
si aspetterà il suo primo parametro in r0), quindi il codice assembly "ottimizzato" avrebbe bisogno di cinque istruzioni. Anche se il compilatore non fosse a conoscenza di registri che contengono l'indirizzo o il valore da memorizzare, la sua versione a quattro istruzioni (che potrebbe adattare per usare qualsiasi registro disponibile - non necessariamente r0 e r1) batterebbe l'assemblaggio "ottimizzato" -language version. Se il compilatore avesse l'indirizzo e i dati necessari in r5 e r7 come descritto in precedenza, function1
non altererebbe tali registri e quindi potrebbe sostituireset_port_high
con un'unica strb
istruzione:quattro istruzioni più piccole e veloci rispetto al codice assembly "ottimizzato a mano".
Si noti che il codice assembly ottimizzato a mano può spesso sovraperformare un compilatore nei casi in cui il programmatore conosce il flusso preciso del programma, ma i compilatori brillano nei casi in cui un pezzo di codice viene scritto prima che il suo contesto sia noto o in cui un pezzo di codice sorgente può essere invocato da più contesti [se set_port_high
utilizzato in cinquanta posizioni diverse nel codice, il compilatore potrebbe decidere autonomamente per ognuno di quelli il modo migliore per espanderlo].
In generale, suggerirei che il linguaggio assembly è suscettibile di produrre i migliori miglioramenti delle prestazioni nei casi in cui ogni parte di codice può essere affrontata da un numero molto limitato di contesti ed è dannosa per le prestazioni in luoghi in cui una parte di il codice può essere affrontato da molti contesti diversi. In modo interessante (e convenientemente) i casi in cui l'assemblaggio è più vantaggioso per le prestazioni sono spesso quelli in cui il codice è più semplice e facile da leggere. I luoghi in cui il codice della lingua dell'assembly si trasformerebbe in un pasticcio appiccicoso sono spesso quelli in cui la scrittura in assembly offrirebbe il minimo vantaggio in termini di prestazioni.
[Nota minore: ci sono alcuni punti in cui è possibile utilizzare il codice assembly per produrre un pasticcio appiccicoso iper-ottimizzato; ad esempio, un pezzo di codice che ho fatto per l'ARM doveva recuperare una parola dalla RAM ed eseguire una delle dodici routine in base ai sei bit superiori del valore (molti valori mappati sulla stessa routine). Penso di aver ottimizzato quel codice in modo simile a:
ldrh r0,[r1],#2! ; Fetch with post-increment
ldrb r1,[r8,r0 asr #10]
sub pc,r8,r1,asl #2
Il registro r8 conteneva sempre l'indirizzo della tabella di invio principale (all'interno del ciclo in cui il codice trascorre il 98% del suo tempo, nulla lo ha mai usato per altri scopi); tutte e 64 le voci si riferivano agli indirizzi nei 256 byte precedenti. Poiché il loop primario aveva nella maggior parte dei casi un limite di tempo di esecuzione di circa 60 cicli, il recupero e il dispaccio in nove cicli è stato molto strumentale per raggiungere quell'obiettivo. L'uso di una tabella di 256 indirizzi a 32 bit sarebbe stato un ciclo più veloce, ma avrebbe assorbito 1 KB di RAM molto preziosa [il flash avrebbe aggiunto più di uno stato di attesa]. L'uso di 64 indirizzi a 32 bit avrebbe richiesto l'aggiunta di un'istruzione per mascherare alcuni bit dalla parola recuperata, e avrebbe comunque inghiottito 192 byte in più rispetto alla tabella che ho effettivamente usato. Utilizzando la tabella degli offset a 8 bit ha prodotto codice molto compatto e veloce, ma non qualcosa che mi aspetterei che un compilatore potrebbe mai inventare; Inoltre, non mi aspetto che un compilatore dedichi un registro "a tempo pieno" per contenere l'indirizzo della tabella.
Il codice sopra è stato progettato per funzionare come un sistema autonomo; potrebbe periodicamente chiamare il codice C, ma solo in determinati momenti in cui l'hardware con cui stava comunicando poteva essere messo in sicurezza in uno stato "inattivo" per due intervalli di circa un millisecondo ogni 16 ms.