Unten ist ein Code, der auf einem 8-Bit-Mikrocontroller implementiert ist.
Der folgende Kommentar wurde zu einer anderen Frage gepostet :
Da Ihr Code die Variable nicht verwendet
i
, warum nicht einfachwhile(length--)
anstelle der erstenfor
Schleife?
Es schlägt vor, die for(..)
Schleife für a zu ändern while(..)
. Wird dies einen praktischen Unterschied machen, wie optimiert der Code für einen eingebetteten Mikrocontroller ist?
uint32_t MyFunction(uint32_t crc, uint8_t *buffer, uint16_t length) {
for (uint16_t i=0; i < length; i++) {
crc = crc ^ *buffer++;
for (uint16_t j=0; j < 8; j++) {
if (crc & 1)
crc = (crc >> 1) ^ 0xEDB88320;
else
crc = crc >> 1;
}
}
return crc;
}
Hinweis: Ich habe diese Frage geschrieben und sie selbst beantwortet, nachdem ich Anfang dieser Woche einen Kommentar gepostet hatte. Dieser Kommentar wurde positiv bewertet, aber ich hatte das Gefühl, ich sollte ihn mit einigen tatsächlichen Tests untermauern. Andere Antworten wären nützlich, aber dies war eher als allgemeiner Beweis für einen Punkt als als eine spezifische Frage zum Algorithmus gedacht.
Die klassischen drei Regeln der Optimierung:
Es schadet nicht, von vornherein sauberen, ordentlichen Code zu schreiben, aber vorzeitige Optimierung ist eine Verschwendung von Zeit und Mühe, die an anderer Stelle sinnvoller eingesetzt werden könnte.
Zunächst ist zu überlegen, was optimiert werden muss. Benötigt der Code weniger Programmspeicher oder soll er schneller laufen? Manchmal können diese Dinge beide erreicht werden, indem die Anzahl der Befehle reduziert wird, dies hängt jedoch vom zugrunde liegenden Mikrocontroller und der Anzahl der Zyklen ab, die jeder Befehl benötigt.
Es ist durchaus möglich, kleineren Code zu generieren, der mehr Zyklen zum Ausführen benötigt, insbesondere wenn Verzweigungen beteiligt sind.
Um mit dem Experimentieren zu beginnen, ist es wichtig, den kleinstmöglichen Codeabschnitt zu isolieren. Ich habe den folgenden einfachen Testfall erstellt, der die Funktion aus der Frage enthält und zwischen verschiedenen Mikrocontroller-Familien portierbar ist:
#include <stdint.h>
uint32_t MyFunction(uint32_t crc, uint8_t *buffer, uint16_t length) {
uint16_t i;
uint16_t j;
for (i = 0; i < length; i++) {
crc = crc ^ *buffer++;
for (j = 0; j < 8; j++) {
if (crc & 1)
crc = (crc >> 1) ^ 0xEDB88320;
else
crc = crc >> 1;
}
}
return crc;
}
int main(int argc, char** argv) {
uint8_t data[] = "ABCDEF";
uint32_t ret = 0;
ret = MyFunction(ret, data, 6);
while(1);
}
Wie in den Kommentaren zur anderen Frage erwähnt, wird der Wert der Variablen i
niemals direkt verwendet, sondern immer nur mit verglichen length
. Daher könnten wir es wie folgt umschreiben:
uint32_t MyFunction(uint32_t crc, uint8_t *buffer, uint16_t length) {
uint16_t j;
while (length--) {
// .. do work ..
}
return crc;
}
Zu Vergleichszwecken habe ich avr-gcc Version 4.7.2 (für atmega8) und XC8 1.21 von Microchip (für PIC 18F) verwendet. Für XC8 habe ich PRO-Optimierungen aktiviert, für avr-gcc waren die angegebenen Argumente: avr-gcc -g -c -Os -Wall -mmcu=atmega8 test.c -o test-Os
.
Beachten Sie, dass es wichtig ist zu überprüfen, dass ret
es nicht wegoptimiert ist, da der Compiler denkt, dass es nicht verwendet wird. Beide Varianten des C-Codes generieren dieselbe Assembly aus avr-gcc. Allerdings bemerkt XC8 die ungenutzte Variable nicht, daher kompiliert der Code mit einer while(..)
Schleife auf 4 Bytes kleiner mit 2 RAM-Bytes gespart.
Wie in einem Kommentar zu dieser Frage erwähnt, wäre es auch effizienter, j
eine zu erstellen uint8_t
, da die Eingabe nie breiter als 8 Bit sein wird. Tests auf XC8 zeigen, dass durch diese Änderung weitere 8 Byte Programmplatz und 1 Byte RAM eingespart werden. Es reduziert auch die mit avr-gcc erzeugte Ausgabe um 4 Bytes und ein Registerbyte.
Zusammenfassend lohnt es sich immer, dem Compiler die beste Chance zu geben, guten Code zu generieren , in diesem Fall, indem keine zusätzlichen unnötigen Variablen verwendet werden und die kleinstmögliche Speicherklasse verwendet wird. Einige optimierende Compiler kommen besser zurecht als andere, aber beide funktionieren gut, wenn der eingegebene C-Code gut durchdacht ist.
So etwas kann auf vielen Prozessoren in eine kleine Routine in Assemblersprache umgeschrieben werden, die viel schneller läuft als jede mögliche C-Implementierung. Auf einem PIC könnte die CRC-Berechnung beispielsweise mit 72 Anweisungen geschrieben werden, wenn sich _crc in der aktuell ausgewählten Bank befindet (Code für PICs der 16F-Serie).
movf _crc+1,w
btfss _crc,0
xorlw 0xNN ; Would need to figure out proper value
btfss _crc,1
xorlw 0xNN ; Would need to figure out proper value
.. six more such instruction pairs
movwf btemp+0 ; LSB of return
movf _crc+2,w
btfss _crc,0
xorlw 0xNN ; Would need to figure out proper value
btfss _crc,1
xorlw 0xNN ; Would need to figure out proper value
.. six more such instruction pairs
movwf btemp+1 ; Next byte of return
movf _crc+3,w
.. eight more instruction pairs
movwf btemp+2,w
movlw 0
.. eight more instruction pairs
movwf btemp+3
Nicht das kompakteste Ding der Welt, aber es würde den CRC32 für ein eingehendes Byte in 72 Zyklen aktualisieren. Wenn man alternativ ein Teil der 18F-Serie verwendet und den Coderaum sparen könnte, könnte man Tabellen im Wert von 1 KByte verwenden (organisiert als vier seitenausgerichtete 256-Byte-Stücke). Code wäre dann so etwas wie:
movf _crc,w,b
movwf _TBLPTRL,c
movlw TabUpperAddress
movwf _TBLPTRU,c
movlw TabHighAddress
movwf _TBLPTRH,c
tblrd *
movff _TABLAT,btemp+3
incf _TBLPTRH,c
tblrd *
xorwf _crc+1,w,b
movff _TABLAT,btemp+0
incf _TBLPTRH,c
tblrd *
xorwf _crc+2,w,b
movff _TABLAT,btemp+1
incf _TBLPTRH,c
tblrd *
xorwf _crc+3,w,b
movff _TABLAT,btemp+2
Etwas schneller, aber es würde zusätzlich zum Code selbst Datentabellen im Wert von 1 KByte erfordern.
Olin Lathrop
Renan
Apalopohapa
David
David
David