In vielen Fällen kann die optimale Ausführung einer Aufgabe vom Kontext abhängen, in dem die Aufgabe ausgeführt wird. Wenn eine Routine in Assemblersprache geschrieben ist, ist es im Allgemeinen nicht möglich, die Reihenfolge der Anweisungen je nach Kontext zu variieren. Betrachten Sie als einfaches Beispiel die folgende einfache Methode:
inline void set_port_high(void)
{
(*((volatile unsigned char*)0x40001204) = 0xFF);
}
Ein Compiler für 32-Bit-ARM-Code würde ihn angesichts der obigen Ausführungen wahrscheinlich wie folgt rendern:
ldr r0,=0x40001204
mov r1,#0
strb r1,[r0]
[a fourth word somewhere holding the constant 0x40001204]
oder vielleicht
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]
Das könnte im handmontierten Code leicht optimiert werden, wie entweder:
ldr r0,=0x400011FF
strb r0,[r0+5]
[a third word somewhere holding the constant 0x400011FF]
oder
mvn r0,#0xC0 ; Load with 0x3FFFFFFF
add r0,r0,#0x1200 ; Add 0x1200, yielding 0x400011FF
strb r0,[r0+5]
Beide von Hand zusammengestellten Ansätze würden 12 Bytes Code-Speicherplatz anstelle von 16 erfordern; Letzteres würde ein "Laden" durch ein "Hinzufügen" ersetzen, das auf einem ARM7-TDMI zwei Zyklen schneller ausführen würde. Wenn der Code in einem Kontext ausgeführt werden würde, in dem r0 nicht bekannt / egal ist, wären die Assembler-Versionen daher etwas besser als die kompilierte Version. Angenommen, der Compiler wusste, dass ein Register [z. B. r5] einen Wert enthalten würde, der innerhalb von 2047 Bytes der gewünschten Adresse 0x40001204 [z. B. 0x40001000] liegt, und wusste weiter, dass ein anderes Register [z. B. r7] ausgeführt wird um einen Wert zu halten, dessen niedrige Bits 0xFF waren. In diesem Fall könnte ein Compiler die C-Version des Codes optimieren, um einfach:
strb r7,[r5+0x204]
Viel kürzer und schneller als selbst der handoptimierte Assembler-Code. Angenommen, set_port_high ist im Kontext aufgetreten:
int temp = function1();
set_port_high();
function2(temp); // Assume temp is not used after this
Überhaupt nicht unplausibel beim Codieren für ein eingebettetes System. Wenn set_port_high
es in Assembly-Code geschrieben ist, müsste der Compiler r0 (das den Rückgabewert von enthält function1
) an eine andere Stelle verschieben, bevor er den Assembly-Code aufruft, und diesen Wert anschließend wieder auf r0 verschieben (dafunction2
der erste Parameter in r0 erwartet wird). Der "optimierte" Assembler-Code würde also fünf Anweisungen benötigen. Selbst wenn der Compiler keine Register kennen würde, die die Adresse oder den zu speichernden Wert enthalten, würde seine Version mit vier Befehlen (die er anpassen könnte, um alle verfügbaren Register zu verwenden - nicht unbedingt r0 und r1) die "optimierte" Assembly schlagen -sprachige Version. Wenn der Compiler die erforderlichen Adressen und Daten in r5 und r7 hätte, wie zuvor beschrieben, function1
würde er diese Register nicht ändern und könnte sie somit ersetzenset_port_high
mitstrb
Anweisung -vier Anweisungen kleiner und schneller als der "handoptimierte" Assembler-Code.
Beachten Sie, dass handoptimierter Assembler-Code einen Compiler häufig übertreffen kann, wenn der Programmierer den genauen Programmablauf kennt, Compiler jedoch in Fällen glänzen, in denen ein Teil des Codes geschrieben wurde, bevor sein Kontext bekannt ist, oder wenn ein Teil des Quellcodes vorhanden ist aus mehreren Kontexten aufgerufen [if set_port_high
der Compiler an fünfzig verschiedenen Stellen im Code verwendet wird, kann er unabhängig für jeden von ihnen entscheiden, wie er am besten erweitert werden soll].
Im Allgemeinen würde ich vorschlagen, dass die Assemblersprache in den Fällen, in denen jeder Code aus einer sehr begrenzten Anzahl von Kontexten heraus aufgerufen werden kann, die größten Leistungsverbesserungen erzielt und die Leistung an Orten beeinträchtigt, an denen ein Teil von Code vorhanden ist Code kann aus vielen verschiedenen Kontexten betrachtet werden. Interessanterweise (und bequemerweise) sind die Fälle, in denen die Montage für die Leistung am vorteilhaftesten ist, häufig diejenigen, in denen der Code am einfachsten und am einfachsten zu lesen ist. Die Stellen, an denen Assembler-Code zu einem Durcheinander wird, sind häufig diejenigen, an denen das Schreiben in Assembler den geringsten Leistungsvorteil bietet.
[Kleiner Hinweis: Es gibt einige Stellen, an denen Assembler-Code verwendet werden kann, um ein hyperoptimiertes, klebriges Durcheinander zu erzielen. Zum Beispiel musste ein Code, den ich für den ARM gemacht habe, ein Wort aus dem RAM abrufen und eine von ungefähr zwölf Routinen basierend auf den oberen sechs Bits des Werts ausführen (viele Werte sind derselben Routine zugeordnet). Ich glaube, ich habe diesen Code so optimiert:
ldrh r0,[r1],#2! ; Fetch with post-increment
ldrb r1,[r8,r0 asr #10]
sub pc,r8,r1,asl #2
Das Register r8 enthielt immer die Adresse der Hauptversandtabelle (innerhalb der Schleife, in der der Code 98% seiner Zeit verbringt, wurde er nie für einen anderen Zweck verwendet); Alle 64 Einträge beziehen sich auf Adressen in den 256 Bytes davor. Da die primäre Schleife in den meisten Fällen ein hartes Ausführungszeitlimit von etwa 60 Zyklen hatte, war das Abrufen und Versenden von neun Zyklen sehr hilfreich, um dieses Ziel zu erreichen. Die Verwendung einer Tabelle mit 256 32-Bit-Adressen wäre einen Zyklus schneller gewesen, hätte jedoch 1 KB sehr wertvollen Arbeitsspeicher verschlungen [Flash hätte mehr als einen Wartezustand hinzugefügt]. Die Verwendung von 64 32-Bit-Adressen hätte das Hinzufügen eines Befehls zum Maskieren einiger Bits aus dem abgerufenen Wort erforderlich gemacht und immer noch 192 Bytes mehr verschlungen als die Tabelle, die ich tatsächlich verwendet habe. Die Verwendung der Tabelle der 8-Bit-Offsets ergab einen sehr kompakten und schnellen Code. aber nicht etwas, von dem ich erwarten würde, dass ein Compiler es jemals erfinden würde; Ich würde auch nicht erwarten, dass ein Compiler ein Register "Vollzeit" für das Halten der Tabellenadresse reserviert.
Der obige Code wurde als eigenständiges System ausgeführt. Es konnte regelmäßig C-Code aufrufen, jedoch nur zu bestimmten Zeiten, zu denen die Hardware, mit der es kommunizierte, alle 16 ms für zwei Intervalle von ungefähr einer Millisekunde sicher in den Ruhezustand versetzt werden konnte.