Ich habe an der Entwicklung einer Funktion für ein bestimmtes Produkt von uns gearbeitet. Es wurde angefordert, dasselbe Feature auf ein anderes Produkt zu portieren. Dieses Produkt basiert auf einem M16C-Mikrocontroller, der traditionell über 64 KB Flash und 2 KB RAM verfügt.
Es ist ein ausgereiftes Produkt und hat daher nur noch 132 Bytes Flash und 2 Bytes RAM.
Um die angeforderte Funktion zu portieren (die Funktion selbst wurde optimiert), benötige ich 1400 Bytes Flash und ~ 200 Bytes RAM.
Hat jemand Vorschläge, wie man diese Bytes durch Codekompaktierung abruft? Nach welchen speziellen Dingen suche ich, wenn ich bereits vorhandenen Arbeitscode komprimieren möchte?
Irgendwelche Ideen werden wirklich geschätzt.
Vielen Dank.
Antworten:
Sie haben mehrere Möglichkeiten: Suchen Sie zunächst nach redundantem Code und verschieben Sie ihn in einen einzelnen Aufruf, um die Duplizierung zu beseitigen. Die zweite ist, die Funktionalität zu entfernen.
Sehen Sie sich Ihre .map-Datei genau an und prüfen Sie, ob Funktionen vorhanden sind, die Sie entfernen oder neu schreiben können. Stellen Sie außerdem sicher, dass die verwendeten Bibliotheksaufrufe wirklich benötigt werden.
Bestimmte Dinge wie Division und Multiplikation können viel Code einbringen, aber die Verwendung von Shifts und eine bessere Verwendung von Konstanten kann den Code verkleinern. Schauen Sie sich auch Dinge wie String-Konstanten und
printf
s an. Zum Beispielprintf
frisst jeder Ihr Rom auf, aber Sie können möglicherweise ein paar Zeichenfolgen mit gemeinsamem Format verwenden, anstatt diese Zeichenfolgenkonstante immer wieder zu wiederholen.Für den Speicher sehen Sie, ob Sie Globals loswerden und stattdessen Autos in einer Funktion verwenden können. Vermeiden Sie außerdem so viele Variablen in der Hauptfunktion wie möglich, da diese den Speicher genau wie globale Variablen aufbrauchen.
quelle
Es lohnt sich immer, die Ausgabe von Listing Files (Assembler) nach Dingen zu durchsuchen, bei denen Ihr bestimmter Compiler besonders schlecht ist.
Beispielsweise stellen Sie möglicherweise fest, dass lokale Variablen sehr teuer sind, und wenn die Anwendung so einfach ist, dass sie das Risiko wert ist, kann das Verschieben einiger Schleifenzähler in statische Variablen viel Code sparen.
Oder Array-Indizierung kann sehr teuer sein, aber Zeigeroperationen viel billiger. Oder umgekehrt.
Ein Blick auf die Assemblersprache ist jedoch der erste Schritt.
quelle
Compiler-Optimierungen, zum Beispiel
-Os
in GCC, bieten die beste Balance zwischen Geschwindigkeit und Codegröße. Vermeiden Sie-O3
, da dies die Codegröße erhöhen kann.quelle
Überprüfen Sie für RAM den Bereich aller Variablen. Verwenden Sie Ints, in denen Sie ein Zeichen verwenden können? Sind die Puffer größer als nötig?
Das Auspressen von Code ist sehr anwendungs- und codierungsstilabhängig. Ihre verbleibenden Beträge deuten darauf hin, dass der Code möglicherweise schon ein wenig zusammengedrückt wurde, was bedeuten könnte, dass nur noch wenig zu haben ist.
Schauen Sie sich auch die Gesamtfunktionalität genau an - gibt es etwas, das nicht wirklich verwendet wird und über Bord geworfen werden kann?
quelle
Wenn es sich um ein altes Projekt handelt, der Compiler jedoch seitdem entwickelt wurde, kann es sein, dass ein neuerer Compiler kleineren Code erzeugt
quelle
Es lohnt sich immer, in Ihrem Compiler-Handbuch nach Optionen zu suchen, um den Speicherplatz zu optimieren.
Für gcc
-ffunction-sections
und-fdata-sections
mit dem--gc-sections
Linker-Flag ist es gut, toten Code zu entfernen.Hier sind einige andere hervorragende Tipps (auf AVR ausgerichtet)
quelle
Sie können die Menge des zugewiesenen Stapel- und Heapspeichers untersuchen. Möglicherweise können Sie eine erhebliche Menge an RAM zurückerhalten, wenn einer oder beide überbelegt sind.
Ich vermute, für ein Projekt, das in 2 KB RAM passt, gibt es zunächst keine dynamische Speicherzuweisung (Verwendung von
malloc
,calloc
etc.). Wenn dies der Fall ist, können Sie Ihren Heap insgesamt entfernen, vorausgesetzt, der ursprüngliche Autor hat etwas RAM für den Heap reserviert.Sie müssen sehr vorsichtig sein, um die Stapelgröße zu reduzieren, da dies zu Fehlern führen kann, die sehr schwer zu finden sind. Es kann hilfreich sein, zunächst den gesamten Stapelspeicher auf einen bekannten Wert zu initialisieren (etwas anderes als 0x00 oder 0xff, da diese Werte bereits häufig vorkommen) und dann das System eine Weile laufen zu lassen, um festzustellen, wie viel Stapelspeicher nicht verwendet wird.
quelle
Verwendet Ihr Code Gleitkomma-Mathematik? Möglicherweise können Sie Ihre Algorithmen nur mit ganzzahliger Mathematik erneut implementieren und den Aufwand für die Verwendung der C-Gleitkommabibliothek eliminieren. Beispielsweise können in einigen Anwendungen Funktionen wie Sinus, Log, Exp durch ganzzahlige Polynomapproximationen ersetzt werden.
Verwendet Ihr Code große Nachschlagetabellen für Algorithmen, z. B. CRC-Berechnungen? Sie können versuchen, eine andere Version des Algorithmus zu ersetzen, der die Werte direkt berechnet, anstatt die Nachschlagetabellen zu verwenden. Die Einschränkung ist, dass der kleinere Algorithmus wahrscheinlich langsamer ist, stellen Sie also sicher, dass Sie über genügend CPU-Zyklen verfügen.
Enthält Ihr Code große Mengen an konstanten Daten, z. B. Stringtabellen, HTML-Seiten oder Pixelgrafiken (Symbole)? Wenn es groß genug ist (sagen wir 10 kB), könnte es sich lohnen, ein sehr einfaches Komprimierungsschema zu implementieren, um die Daten zu verkleinern und sie bei Bedarf sofort zu dekomprimieren.
quelle
Sie können versuchen, die Codierung zu ändern, um einen kompakteren Stil zu erzielen. Es hängt sehr davon ab, was der Code tut. Der Schlüssel ist, ähnliche Dinge zu finden und sie in Bezug aufeinander neu zu implementieren. Ein Extrem wäre die Verwendung einer höheren Programmiersprache wie Forth, mit der es einfacher ist, eine höhere Codedichte als in C oder Assembler zu erzielen.
Hier ist Forth für M16C .
quelle
Legen Sie die Optimierungsstufe des Compilers fest. Viele IDEs verfügen über Einstellungen, die eine Optimierung der Codegröße auf Kosten der Kompilierungszeit (oder in einigen Fällen sogar der Verarbeitungszeit) ermöglichen. Sie können die Code-Komprimierung durchführen, indem Sie das Optimierungsprogramm einige Male erneut ausführen und nach seltenen Mustern suchen, die für die Optimierung optimiert werden können, sowie nach einer ganzen Reihe weiterer Tricks, die für die Kompilierung unter normalen Umständen nicht erforderlich sind. Normalerweise sind Compiler standardmäßig auf einen mittleren Optimierungsgrad eingestellt. Stöbern Sie in den Einstellungen und finden Sie eine ganzzahlige Optimierungsskala.
quelle
Wenn Sie bereits einen professionellen Compiler wie IAR verwenden, werden Sie wahrscheinlich Schwierigkeiten haben, durch geringfügige Code-Optimierungen auf niedriger Ebene ernsthafte Einsparungen zu erzielen. Sie müssen sich mehr mit dem Entfernen von Funktionen oder der Ausführung von Hauptfunktionen befassen Effizienteres Umschreiben von Teilen. Sie müssen ein intelligenterer Programmierer sein als derjenige, der die ursprüngliche Version geschrieben hat. Was den Arbeitsspeicher betrifft, müssen Sie einen genauen Blick darauf werfen, wie dieser derzeit verwendet wird, und prüfen, ob die Verwendung desselben Arbeitsspeichers überlagert werden kann verschiedene dinge zu verschiedenen zeiten (gewerkschaften sind dafür praktisch). Die Standard-Heap- und -Stack-Größen von IAR in den ARM / AVR-Größen, die ich tendenziell zu großzügig gewählt habe, sollten als Erstes betrachtet werden.
quelle
Etwas anderes zu überprüfen - einige Compiler auf einigen Architekturen Konstanten RAM kopieren - in der Regel verwendet , wenn der Zugriff auf Flash - Konstanten langsam / schwer ist (zB AVR) zB IAR AVR - Compiler erfordert eine _ _flash Qualifer bis nicht eine Konstante RAM kopieren)
quelle
Wenn Ihr Prozessor keine Hardwareunterstützung für einen Parameter- / lokalen Stapel hat, der Compiler jedoch trotzdem versucht, einen Laufzeitparameterstapel zu implementieren, und wenn Ihr Code nicht erneut eingegeben werden muss, können Sie möglicherweise Code speichern Raum durch statische Zuordnung von Auto-Variablen. In einigen Fällen muss dies manuell erfolgen. In anderen Fällen können Compiler-Direktiven dies tun. Eine effiziente manuelle Zuordnung erfordert die gemeinsame Nutzung von Variablen zwischen den Routinen. Diese Freigabe muss sorgfältig erfolgen, um sicherzustellen, dass keine Routine eine Variable verwendet, die von einer anderen Routine als "im Bereich" eingestuft wird. In einigen Fällen können die Vorteile der Codegröße jedoch erheblich sein.
Einige Prozessoren haben Aufrufkonventionen, mit denen einige Parameterübergabestile effizienter als andere sind. Wenn beispielsweise bei den PIC18-Controllern eine Routine einen einzelnen Ein-Byte-Parameter akzeptiert, kann dieser in einem Register übergeben werden. wenn es mehr braucht, alles Parameter im RAM übergeben werden. Wenn eine Routine zwei Ein-Byte-Parameter verwenden würde, ist es möglicherweise am effizientesten, einen in einer globalen Variablen und dann den anderen als Parameter zu übergeben. Bei weit verbreiteten Routinen können sich die Einsparungen summieren. Sie können besonders wichtig sein, wenn der über global übergebene Parameter ein Einzelbit-Flag ist oder normalerweise den Wert 0 oder 255 hat (da spezielle Anweisungen zum Speichern einer 0 oder 255 im RAM vorhanden sind).
Auf dem ARM kann das Zusammenfügen von häufig verwendeten globalen Variablen zu einer Struktur die Codegröße erheblich verringern und die Leistung verbessern. Wenn A, B, C, D und E separate globale Variablen sind, muss der Code, der sie alle verwendet, die Adresse jeder Variablen in ein Register laden. Wenn nicht genügend Register vorhanden sind, müssen diese Adressen möglicherweise mehrmals neu geladen werden. Wenn sie hingegen Teil derselben globalen Struktur MyStuff sind, kann Code, der MyStuff.A, MyStuff.B usw. verwendet, die Adresse von MyStuff einfach einmal laden. Großer Gewinn.
quelle
1.Wenn Ihr Code auf vielen Strukturen basiert, stellen Sie sicher, dass die Strukturelemente in der Reihenfolge angeordnet sind, in der sie den meisten Speicher belegen.
Beispiel: "uint32_t uint16_t uint8_t" anstelle von "uint16_t uint8_t uint32_t"
Dies stellt ein Minimum an Strukturpolsterung sicher.
2.Verwenden Sie ggf. const für Variablen. Dadurch wird sichergestellt, dass sich diese Variablen im ROM befinden und nicht den RAM aufbrauchen
quelle
Ein paar (vielleicht offensichtliche) Tricks, mit denen ich den Code eines Kunden erfolgreich komprimiert habe:
Kompakte Flags in Bitfelder oder Bitmasken. Dies kann von Vorteil sein, da normalerweise Boolesche Werte als Ganzzahlen gespeichert werden, wodurch Speicher verschwendet wird. Dies spart sowohl RAM als auch ROM und wird normalerweise nicht vom Compiler durchgeführt.
Suchen Sie nach Redundanz im Code und verwenden Sie Schleifen oder Funktionen, um wiederholte Anweisungen auszuführen.
Ich habe auch etwas ROM gespart, indem ich viele
if(x==enum_entry) <assignment>
Anweisungen von Konstanten durch ein indiziertes Array ersetzt habe, indem ich darauf geachtet habe, dass die Enum-Einträge als Array-Index verwendet werden könnenquelle
Verwenden Sie nach Möglichkeit Inline-Funktionen oder Compiler-Makros anstelle kleiner Funktionen. Mit der Übergabe von Argumenten entsteht ein Mehraufwand für Größe und Geschwindigkeit, der behoben werden kann, indem die Funktion inline geschaltet wird.
quelle
int get_a(struct x) {return x.a;}
Ändern Sie die lokalen Variablen auf die gleiche Größe Ihrer CPU-Register.
Wenn die CPU 32-Bit ist, verwenden Sie 32-Bit-Variablen, auch wenn der Maximalwert nie über 255 steigt. Wenn Sie eine 8-Bit-Variable verwendet haben, fügt der Compiler Code hinzu, um die oberen 24 Bit zu maskieren.
Als erstes würde ich die for-loop-Variablen betrachten.
Dies scheint ein guter Ort für eine 8-Bit-Variable zu sein, aber eine 32-Bit-Variable kann weniger Code erzeugen.
quelle