Wie kann man Code für mehr Flash und RAM komprimieren? [geschlossen]

14

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.

IntelliChick
quelle
1
Vielen Dank an alle für die Vorschläge. Ich werde Sie über meine Fortschritte auf dem Laufenden halten und die Schritte auflisten, die funktioniert haben und die nicht.
IntelliChick
Ok, also hier sind die Dinge, die ich versucht habe, die funktioniert haben: Verschobene Compilerversionen. Die Optimierung hatte sich drastisch verbessert, wodurch ich ungefähr 2K Flash hatte. Ging die Listendateien durch, um nach redundanten und nicht verwendeten Funktionen (die aufgrund der gemeinsamen Codebasis geerbt wurden) für das jeweilige Produkt zu suchen, und gewann etwas mehr Flash.
IntelliChick
Für RAM habe ich Folgendes getan: Ich bin die Map-Datei durchgegangen, um Funktionen / Module zu überprüfen, die den meisten RAM verwendeten. Ich fand eine sehr schwere Funktion (12 Kanäle, jeder mit einer festen Menge an zugewiesenem Speicher), Legacy-Code, verstand, was damit erreicht werden sollte, und optimierte die RAM-Nutzung, indem Informationen zwischen den Kanälen ausgetauscht wurden, die üblich waren. Dies gab mir ~ 200 Bytes, die ich brauchte.
IntelliChick
Wenn Sie über ASCII-Dateien verfügen, können Sie die 8- bis 7-Bit-Komprimierung verwenden. Spart Ihnen 12,5%. Die Verwendung einer Zip-Datei würde mehr Code erfordern, um sie zu komprimieren und zu dekomprimieren, als nur zuzulassen.
Sparky256

Antworten:

18

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 printfs an. Zum Beispiel printffrisst 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.

Rex Logan
quelle
1
Vielen Dank für die Vorschläge, ich kann definitiv die meisten davon ausprobieren, mit Ausnahme der für String-Konstanten. Es handelt sich lediglich um ein eingebettetes Gerät ohne Benutzeroberfläche, sodass printf () im Code nicht aufgerufen wird. Ich hoffe, dass diese Vorschläge mich überraschen werden, um meine 1400 Bytes Flash / 200 Bytes RAM zu bekommen, die ich brauche.
IntelliChick
1
@IntelliChick Sie werden erstaunt sein, wie viele Benutzer printf () in einem eingebetteten Gerät verwenden, um entweder zum Debuggen oder zum Senden an ein Peripheriegerät zu drucken. Es scheint, als wüssten Sie das besser, aber wenn jemand vor Ihnen Code für das Projekt geschrieben hat, würde es nicht schaden, danach zu suchen.
Kellenjb
5
Um meinen vorherigen Kommentar zu erweitern, wären Sie auch erstaunt darüber, wie viele Leute Debugging-Anweisungen hinzufügen, diese aber niemals entfernen. Sogar Leute, die #ifdefs machen, werden manchmal immer noch faul.
Kellenjb
1
Super, danke! Ich habe diese Codebasis geerbt, also werde ich auf jeden Fall danach suchen. Ich werde euch über den Fortschritt auf dem Laufenden halten und versuchen zu verfolgen, wie viele Bytes an Speicher oder Flash ich dadurch gewonnen habe, nur als Referenz für alle anderen, die dies möglicherweise in Zukunft tun müssen.
IntelliChick
Nur eine Frage dazu - was ist mit verschachtelten Funktionsaufrufen, die von Schicht zu Schicht springen? Wie viel Aufwand bedeutet das? Ist es besser, die Modularität durch mehrere Funktionsaufrufe beizubehalten oder die Funktionsaufrufe zu reduzieren und einige Bytes zu sparen? Und ist das wichtig?
IntelliChick
8

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
3
Es ist sehr wichtig, dass Sie wissen, was Ihr Compiler macht. Sie sollten sehen, welche Kluft auf meinem Compiler ist. Es bringt Babys zum Weinen (ich selbst eingeschlossen).
Kortuk
8

Compiler-Optimierungen, zum Beispiel -Osin GCC, bieten die beste Balance zwischen Geschwindigkeit und Codegröße. Vermeiden Sie -O3, da dies die Codegröße erhöhen kann.

Thomas O.
quelle
3
Wenn Sie dies tun, müssen Sie ALLES erneut testen! Optimierungen können dazu führen, dass der Arbeitscode aufgrund neuer Annahmen des Compilers nicht funktioniert.
Robert
@Robert, das ist nur wahr, wenn Sie undefinierte Anweisungen verwenden: zB wird a = a ++ in -O0 und -O3 unterschiedlich kompiliert.
Thomas O
5
@ Thomas nicht wahr. Wenn Sie eine for-Schleife haben, um Taktzyklen zu verzögern, werden viele Optimierer erkennen, dass Sie nichts darin tun und es entfernen. Dies ist nur ein Beispiel.
Kellenjb 08.11.10
1
@thomas O, Sie müssen auch darauf achten, dass Sie mit flüchtigen Funktionsdefinitionen vorsichtig sind. Optimierer werden diejenigen in die Luft jagen, die glauben, C gut zu kennen, aber die Komplexität atomarer Operationen nicht verstehen.
Kortuk
1
Alles gute Punkte. Flüchtige Funktionen / Variablen dürfen per Definition NICHT optimiert werden. Jedes Optimierungsprogramm, das Optimierungen daran vornimmt (einschließlich Anrufzeit und Inlining), ist fehlerhaft.
Thomas O
8

Ü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?

mikeselectricstuff
quelle
8

Wenn es sich um ein altes Projekt handelt, der Compiler jedoch seitdem entwickelt wurde, kann es sein, dass ein neuerer Compiler kleineren Code erzeugt

mikeselectricstuff
quelle
Danke Mike! Ich habe dies in der Vergangenheit versucht und der gewonnene Platz wurde bereits genutzt! :) Vom IAR C-Compiler 3.21d auf 3.40 hochgezogen.
IntelliChick
1
Ich bin noch eine Version aufgestiegen und habe es geschafft, ein bisschen mehr Flash zu bekommen, um das Feature zu integrieren. Ich habe jedoch wirklich Probleme mit dem Arbeitsspeicher, der unverändert geblieben ist. :(
IntelliChick
7

Es lohnt sich immer, in Ihrem Compiler-Handbuch nach Optionen zu suchen, um den Speicherplatz zu optimieren.

Für gcc -ffunction-sectionsund -fdata-sectionsmit dem --gc-sectionsLinker-Flag ist es gut, toten Code zu entfernen.

Hier sind einige andere hervorragende Tipps (auf AVR ausgerichtet)

Toby Jaffey
quelle
Funktioniert das eigentlich? In den Dokumenten heißt es: "Wenn Sie diese Optionen angeben, erstellen der Assembler und der Linker größere Objekt- und ausführbare Dateien und sind auch langsamer." Ich verstehe, dass separate Abschnitte für ein Mikro mit Flash- und RAM-Abschnitten sinnvoll sind. Gilt diese Aussage in den Dokumenten nicht für Mikrocontroller?
Kevin Vermeer
Meine Erfahrung ist, dass es gut für AVR funktioniert
Toby Jaffey
1
Dies funktioniert bei den meisten Compilern, die ich verwendet habe, nicht gut. Es ist wie bei der Verwendung des Schlüsselworts register. Sie können dem Compiler mitteilen, dass eine Variable in ein Register aufgenommen wird, aber ein guter Optimierer kann dies viel besser als ein Mensch (argumentieren Sie, wie manche vielleicht, es wird als nicht akzeptabel angesehen, dies in der Praxis zu tun).
Kortuk
1
Wenn Sie mit dem Zuweisen von Speicherorten beginnen, zwingen Sie den Compiler, bestimmte Speicherorte zuzuweisen, was für den erweiterten Bootloader-Code sehr wichtig ist, aber im Optimierer schrecklich zu behandeln ist, da Sie bei der Entscheidungsfindung einen Optimierungsschritt wegnehmen tun könnte. In einigen Compilern ist vorgesehen, dass Abschnitte enthalten sind, für die Code verwendet wird. Dies ist ein Fall, in dem dem Compiler mehr Informationen mitgeteilt werden, um Ihre Verwendung zu verstehen. Dies hilft. Wenn der Compiler es nicht vorschlägt, tun Sie es nicht.
Kortuk
6

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 , callocetc.). 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.

semaj
quelle
Das sind sehr gute Entscheidungen. Ich werde bemerken, dass Sie malloc niemals in einem eingebetteten System verwenden sollten.
Kortuk
1
@Kortuk Das hängt von Ihrer Definition von embedded und der
auszuführenden
1
@joby, ja, das verstehe ich. In einem System mit 0 Neustarts und dem Fehlen eines Betriebssystems wie Linux kann Malloc sehr, sehr schlecht sein.
Kortuk
Es gibt keine dynamische Speicherzuordnung, keinen Ort, an dem malloc, calloc verwendet wird. Ich habe auch die Heap-Zuordnung überprüft und sie wurde bereits auf 0 gesetzt, sodass es keine Heap-Zuordnung gibt. Die derzeit zugewiesene Stack-Größe beträgt 254 Bytes und die Interrupt-Stack-Größe 128 Bytes.
IntelliChick
5

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.

Craig McQueen
quelle
Es gibt 2 kleine Nachschlagetabellen, von denen leider keine 10 KB beträgt. Es wird auch keine Gleitkomma-Mathematik verwendet. :( Vielen Dank für die Vorschläge. Sie sind gut.
IntelliChick
2

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 .

Prof. Falken unterstützt Monica
quelle
2

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.

Joel B
quelle
1
Derzeit auf das Maximum für Größe optimiert. :) Danke für den Vorschlag. :)
IntelliChick
2

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.

mikeselectricstuff
quelle
Danke Mike. Der Code verwendet bereits an den meisten Stellen Gewerkschaften, aber ich werde einige andere Stellen untersuchen, an denen dies möglicherweise noch hilfreich ist. Ich werde auch einen Blick auf die gewählte Stapelgröße werfen und sehen, ob das überhaupt optimiert werden kann.
IntelliChick
Woher weiß ich, welche Stapelgröße angemessen ist?
IntelliChick
2

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)

mikeselectricstuff
quelle
Danke Mike. Ja, das hatte ich bereits überprüft - es heißt "Beschreibbare Konstanten" -Option für den M16C IAR C-Compiler. Es kopiert die Konstanten vom ROM in den RAM. Diese Option ist für mein Projekt deaktiviert. Aber ein wirklich gültiger Scheck! Vielen Dank.
IntelliChick
1

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.

Superkatze
quelle
1

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

AlphaGoku
quelle
1

Ein paar (vielleicht offensichtliche) Tricks, mit denen ich den Code eines Kunden erfolgreich komprimiert habe:

  1. 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.

  2. Suchen Sie nach Redundanz im Code und verwenden Sie Schleifen oder Funktionen, um wiederholte Anweisungen auszuführen.

  3. 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

Clabacchio
quelle
0

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.

AngryEE
quelle
1
Jeder anständige Compiler sollte dies automatisch für Funktionen tun, die nur einmal aufgerufen werden.
mikeselectricstuff
5
Ich habe festgestellt, dass Inlining für Geschwindigkeitsoptimierungen in der Regel nützlicher ist und in der Regel zu Lasten einer größeren Größe geht.
Craig McQueen
Durch Inlining wird in der Regel die Codegröße erhöht, mit Ausnahme von Trivialfunktionen wieint get_a(struct x) {return x.a;}
Dmitry Grigoryev,
0

Ä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.

for( i = 0; i < 100; i++ )

Dies scheint ein guter Ort für eine 8-Bit-Variable zu sein, aber eine 32-Bit-Variable kann weniger Code erzeugen.

Robert
quelle
That may save code but it will eat RAM.
mikeselectricstuff
It will only eat RAM if that function call is in the longest branch of the call trace. Otherwise, it is reusing stack space that some other function already needs.
Robert
2
Usually true providing it's a local variable. If short on RAM, the size of global vars, especially arrays, is a good place to start looking for savings.
mikeselectricstuff
1
Another possibility, interestingly, is to replace unsigned variables with signed. If a compiler optimizes an unsigned short to a 32-bit register, it must add code to ensure that its value wraps from 65535 to zero. If, however, the compiler optimizes a signed short to a register, no such code is required. Because there is no guarantee what will happen if a short is incremented beyond 32767, compilers are not required to emit code to deal with that. On at least two ARM compilers I've looked at, signed-short code can be smaller than unsigned-short code for that reason.
supercat