Ich habe an der Entwicklung einer Funktion für ein bestimmtes Produkt von uns gearbeitet. Es gab eine Anfrage, dieselbe Funktion 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 verfügt daher nur noch über 132 Bytes Flash und 2 Bytes RAM.
Um das angeforderte Feature zu portieren (das Feature selbst wurde optimiert), benötige ich 1400 Bytes Flash und ~200 Bytes RAM.
Hat jemand irgendwelche Vorschläge, wie man diese Bytes durch Codeverdichtung abrufen kann? Auf welche spezifischen Dinge achte ich, wenn ich versuche, bereits vorhandenen Arbeitscode zu komprimieren?
Irgendwelche Ideen werden wirklich geschätzt.
Vielen Dank.
Sie haben mehrere Möglichkeiten: Zuerst suchen Sie nach redundantem Code und verschieben ihn in einen einzelnen Aufruf, um die Duplizierung zu beseitigen. Die zweite besteht darin, die Funktionalität zu entfernen.
Sehen Sie sich Ihre .map-Datei genau an und prüfen Sie, ob es Funktionen gibt, die Sie entfernen oder umschreiben können. Stellen Sie außerdem sicher, dass verwendete Bibliotheksaufrufe wirklich benötigt werden.
Bestimmte Dinge wie Division und Multiplikationen können viel Code einbringen, aber die Verwendung von Verschiebungen und eine bessere Verwendung von Konstanten kann den Code kleiner machen. Sehen Sie sich auch Dinge wie String-Konstanten und printf
s an. Zum Beispiel printf
wird jeder Ihr Rom auffressen, aber Sie können vielleicht ein paar gemeinsame Formatstrings haben, anstatt diese Stringkonstante immer und immer wieder zu wiederholen.
Sehen Sie für den Speicher, ob Sie Globals loswerden und stattdessen Autos in einer Funktion verwenden können. Vermeiden Sie außerdem so viele Variablen wie möglich in der Hauptfunktion, da diese genau wie Globals Speicher verbrauchen.
Es lohnt sich immer, sich die Ausgabe von Listendateien (Assembler) anzusehen, um nach Dingen zu suchen, in denen Ihr bestimmter Compiler besonders schlecht ist.
Beispielsweise stellen Sie möglicherweise fest, dass lokale Variablen sehr teuer sind, und wenn die Anwendung einfach genug ist, um das Risiko wert zu sein, kann das Verschieben einiger Schleifenzähler in statische Variablen eine Menge Code einsparen.
Oder die Indizierung von Arrays könnte sehr teuer sein, aber Zeigeroperationen viel billiger. Oder umgekehrt.
Aber ein Blick auf die Assemblersprache ist der erste Schritt.
Compiler-Optimierungen beispielsweise -Os
in GCC bieten die beste Balance zwischen Geschwindigkeit und Codegröße. Vermeiden Sie -O3
, da es die Codegröße erhöhen kann.
Überprüfen Sie für RAM den Bereich aller Ihrer Variablen - verwenden Sie Ints, wo Sie ein Zeichen verwenden könnten? Sind die Puffer größer als sie sein müssen?
Code Squeezing ist sehr anwendungs- und codierstilabhängig. Ihre verbleibenden Beträge deuten darauf hin, dass der Code möglicherweise bereits durch einige Quetschungen gegangen ist, was bedeuten kann, 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?
Wenn es sich um ein altes Projekt handelt, der Compiler jedoch seitdem entwickelt wurde, kann es sein, dass ein neuerer Compiler möglicherweise kleineren Code erzeugt
Es lohnt sich immer, in Ihrem Compiler-Handbuch nach Optionen zur Platzoptimierung zu suchen.
Für gcc -ffunction-sections
und -fdata-sections
mit dem --gc-sections
Linker-Flag eignen sich gut zum Entfernen von totem Code.
Hier sind einige weitere hervorragende Tipps (ausgerichtet auf AVR)
Sie können den zugewiesenen Stack- und Heap-Speicherplatz untersuchen. Sie können möglicherweise eine beträchtliche Menge an RAM zurückerhalten, wenn einer oder beide überlastet sind.
Meine Vermutung ist, dass es für ein Projekt, das zunächst in 2 KB RAM passt, keine dynamische Speicherzuweisung gibt (Verwendung von malloc
, calloc
, usw.). Wenn dies der Fall ist, können Sie Ihren Heap ganz loswerden, vorausgesetzt, der ursprüngliche Autor hat etwas RAM für den Heap reserviert.
Sie müssen sehr vorsichtig sein, 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 Stack-Speicherplatz auf einen bekannten Wert zu initialisieren (etwas anderes als 0x00 oder 0xff, da diese Werte häufig bereits vorkommen) und dann das System eine Weile laufen zu lassen, um zu sehen, wie viel Stapelspeicherplatz ungenutzt ist.
Verwendet Ihr Code Fließkomma-Mathematik? Möglicherweise können Sie Ihre Algorithmen nur mit ganzzahliger Mathematik neu implementieren und den Overhead der Verwendung der C-Gleitkommabibliothek eliminieren. Beispielsweise können in einigen Anwendungen solche Funktionen wie sin, log, exp durch ganzzahlige polynomische Annäherungen ersetzt werden.
Verwendet Ihr Code große Nachschlagetabellen für Algorithmen wie CRC-Berechnungen? Sie können versuchen, eine andere Version des Algorithmus zu ersetzen, der Werte on-the-fly berechnet, anstatt die Nachschlagetabellen zu verwenden. Die Einschränkung ist, dass der kleinere Algorithmus höchstwahrscheinlich langsamer ist, also stellen Sie sicher, dass Sie genügend CPU-Zyklen haben.
Enthält Ihr Code große Mengen konstanter Daten, z. B. Zeichenfolgentabellen, HTML-Seiten oder Pixelgrafiken (Symbole)? Wenn es groß genug ist (z. B. 10 kB), könnte es sich lohnen, ein sehr einfaches Komprimierungsschema zu implementieren, um die Daten zu verkleinern und bei Bedarf spontan zu dekomprimieren.
Sie können versuchen, den Code in einem kompakteren Stil neu anzuordnen. Es hängt viel davon ab, was der Code tut. Der Schlüssel liegt darin, ähnliche Dinge zu finden und sie in Bezug aufeinander neu zu implementieren. Ein Extrem wäre die Verwendung einer höheren Sprache wie Forth, mit der es einfacher sein kann, eine höhere Codedichte zu erreichen als in C oder Assembler.
Hier ist Forth für M16C .
Legen Sie die Optimierungsstufe des Compilers fest. Viele IDEs haben Einstellungen, die eine Optimierung der Codegröße auf Kosten der Kompilierzeit (oder in einigen Fällen vielleicht sogar der Verarbeitungszeit) ermöglichen. Sie können Code komprimieren, indem sie ihren Optimierer ein paar Mal erneut ausführen, nach weniger häufigen optimierbaren Mustern suchen und eine ganze Reihe anderer Tricks anwenden, die für die gelegentliche/Debug-Kompilierung möglicherweise nicht erforderlich sind. Normalerweise sind Compiler standardmäßig auf eine mittlere Optimierungsstufe eingestellt. Graben Sie in den Einstellungen herum, und Sie sollten in der Lage sein, eine auf Ganzzahlen basierende Optimierungsskala zu finden.
Wenn Sie bereits einen professionellen Compiler wie IAR verwenden, werden Sie meiner Meinung nach Schwierigkeiten haben, ernsthafte Einsparungen durch geringfügige Low-Level-Code-Optimierungen zu erzielen - Sie müssen eher darauf achten, Funktionen zu entfernen oder größere Änderungen vorzunehmen Umschreiben von Teilen auf effizientere Weise. Sie müssen ein klügerer Programmierer sein als derjenige, der die Originalversion geschrieben hat ... Was den Arbeitsspeicher betrifft, müssen Sie sich sehr genau ansehen, wie er derzeit verwendet wird, und prüfen, ob es Spielraum für eine überlagerte Verwendung des gleichen Arbeitsspeichers gibt 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 übertrieben habe, sind also das erste, was Sie sich ansehen sollten.
Etwas anderes zu überprüfen - einige Compiler auf einigen Architekturen kopieren Konstanten in den RAM - wird normalerweise verwendet, wenn der Zugriff auf Flash-Konstanten langsam/schwierig ist (z. B. AVR), z. B. der AVR-Compiler von IAR erfordert einen _ _flash-Qualifer, um eine Konstante nicht in den RAM zu kopieren)
Wenn Ihr Prozessor keine Hardwareunterstützung für einen Parameter-/lokalen Stapel hat, der Compiler aber trotzdem versucht, einen Laufzeitparameterstapel zu implementieren, und wenn Ihr Code nicht wiedereintrittsfähig sein muss, können Sie möglicherweise Code speichern Speicherplatz durch statische Zuweisung von Auto-Variablen. In manchen 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 Routinen. Eine solche gemeinsame Nutzung muss sorgfältig durchgeführt werden, um sicherzustellen, dass keine Routine eine Variable verwendet, die eine andere Routine als "im Geltungsbereich" betrachtet, aber in einigen Fällen können die Vorteile der Codegröße erheblich sein.
Einige Prozessoren haben Aufrufkonventionen, die einige Parameterübergabestile effizienter machen können als andere. Wenn beispielsweise bei den PIC18-Controllern eine Routine einen einzelnen Ein-Byte-Parameter verwendet, kann dieser in einem Register übergeben werden; braucht es mehr, müssen alle Parameter im RAM übergeben werden. Wenn eine Routine zwei Ein-Byte-Parameter annehmen würde, kann es am effizientesten sein, einen in eine globale Variable zu "übergeben" 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 Ein-Bit-Flag ist oder normalerweise einen Wert von 0 oder 255 hat (da es spezielle Anweisungen gibt, um eine 0 oder 255 im RAM zu speichern).
Auf dem ARM kann das Einfügen globaler Variablen, die häufig zusammen in eine Struktur verwendet werden, die Codegröße erheblich reduzieren und die Leistung verbessern. Wenn A, B, C, D und E separate globale Variablen sind, muss Code, der alle verwendet, die Adresse von jedem in ein Register laden; Wenn nicht genügend Register vorhanden sind, müssen diese Adressen möglicherweise mehrmals neu geladen werden. Wenn sie dagegen Teil derselben globalen Struktur MyStuff sind, kann Code, der MyStuff.A, MyStuff.B usw. verwendet, einfach die Adresse von MyStuff einmal laden. Großer Gewinn.
1.Wenn Ihr Code auf vielen Strukturen beruht, stellen Sie sicher, dass die Strukturmitglieder von denen, die den meisten Speicher belegen, bis zu den wenigsten geordnet sind.
Bsp.: „uint32_t uint16_t uint8_t“ statt „uint16_t uint8_t uint32_t“
Dadurch wird eine minimale Strukturpolsterung gewährleistet.
2.Verwenden Sie gegebenenfalls const für Variablen. Dadurch wird sichergestellt, dass sich diese Variablen im ROM befinden und keinen RAM verbrauchen
Ein paar (vielleicht offensichtliche) Tricks, die ich erfolgreich beim Komprimieren des Codes einiger Kunden angewendet habe:
Merker in Bitfelder oder Bitmasken komprimieren. Dies kann vorteilhaft sein, da boolesche Werte normalerweise als Ganzzahlen gespeichert werden und somit Speicherplatz verschwenden. 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önnen
Verwenden Sie nach Möglichkeit Inline-Funktionen oder Compiler-Makros anstelle von kleinen Funktionen. Es gibt einen Größen- und Geschwindigkeits-Overhead beim Übergeben von Argumenten und dergleichen, der behoben werden kann, indem die Funktion inline gemacht wird.
int get_a(struct x) {return x.a;}
Ändern Sie die lokalen Variablen so, dass sie dieselbe Größe wie Ihre CPU-Register haben.
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.
Der erste Ort, an dem ich nachsehen würde, sind die Variablen der for-Schleife.
for( i = 0; i < 100; i++ )
Dies scheint ein guter Ort für eine 8-Bit-Variable zu sein, aber eine 32-Bit-Variable erzeugt möglicherweise weniger Code.
IntelliChick
IntelliChick
IntelliChick
Benutzer105652