PDA

Archiv verlassen und diese Seite im Standarddesign anzeigen : [ERLEDIGT] Softwaretest mit Code Optimierung



Siro
27.10.2015, 08:01
Einen schönen guten Morgen,

mich beschäftigt grade eine Frage wegen SOftwaretests.

Ich habe eine C Software für ein Gerät "fertig ???" geschrieben.
Nach ausgiebigen Tests "scheint" diese korrekt zu funktionieren.

Wenn ich nun selbige Software mit der Compiler Option O1 O2 (sprich Optimierungen) compiliere
funktioiert so gut wie garnichts mehr.

Jetzt die Frage:

Muss eine C Software mit sämtlichen Compileroptionen getestet und lauffähig gemacht werden ?

PS:
Warum da einiges nicht mehr funktioniert hat verschiedene Ursachen, die ich schon teiweise ermitteln konnte.

Prozessorspezifische Interrupt Eigenschaften, plötzlich "volatile" erforderlich, Code wegoptimiert.....

Ich bekomme sie sicherlich auch in jeder Optimierung zum Laufen, darum geht es nicht.

Ist das erforderlich ist meine Frage. Zudem könnte ich mir vorstellen, dass ein Compiler von beispielsweise IAR
eine anderen Assemblercode erzeugt als ein GNU Compiler, bei Optimierungen ebenfalls Unterschiede entstehen.

Danke Euch für Informationen.

Siro

oberallgeier
27.10.2015, 08:44
Hallo Siro,

meine eigenen Erfahrungen (und mein Wissen) dazu sind bescheiden. ABER - meine aktuelle Platinenbestückung für meinen Archie ist doch recht komplex, zwölf mit UART oder I²C-verbundene Platinen, davon sechs mit z.T. ziemlich interruptdurchseuchtem Code.
.. Wenn ich nun selbige Software mit der Compiler Option O1 O2 (sprich Optimierungen) compiliere
funktioiert so gut wie garnichts mehr ..
Muss eine C Software mit sämtlichen Compileroptionen getestet und lauffähig gemacht werden ? ..Programmier-IDE ist bei mir Studio 4, optimiert wird immer mit -Os. Das hat natürlich auch die Notwendigkeit dass ich so ziemlich alle irgendwie "verdächtige" Variablen als volatile deklariere, wenns sinnvoll erscheint dann wird das natürlich auch mit Startwerten definiert. Gerade "Startwerte" sind so ne Sache. Die NOtwendigkeit wird nicht IMMER geprüft - oft nur aus der Erfahrung vermutet. Da könnten ja zur Laufzeit Variablenwerte (die nicht-atomaren) in Interruptserviceroutinen verändert werden - während sie in der gerade unterbrochenen Routine bearbeitet werden. Und das kann der Compiler so optimieren, dass Du Trouble bekommst. Auch ohne Optimierung sind gerade nicht-atomare Operationen sehr empfindlich auf Interrupts.

Eine gewisse Überprüfung - allereinfachster Art - wäre natürlich die Angabe des Compilers über den verbrauchten Speicherplatz als Flash und SRAM (also Program und Data) bei verschiedenen Optimierungen zu vergleichen. Steht die Optimierung überhaupt dafür - im Sinne eines sparsameren Codes? BRAUCHST Du sparsameren Code? Andererseits - sehr geschachtelte Routinen und/oder reichlich ISR brauchen jede Menge Stack. Da gibts empfehlungen die oberen 20 oder mehr Prozent SRAM frei zu halten - ich habe auch schon Empfehlungen von bis zu 50 Prozent gelesen.

(M)EIN knappes, einfachs Fazit also: "verdächtige" oder eben mehrfach benutzte Variablen als volatile deklarieren erspart manchen Kummer.

Peter(TOO)
27.10.2015, 10:22
Hallo Siro,

Muss eine C Software mit sämtlichen Compileroptionen getestet und lauffähig gemacht werden ?

PS:
Warum da einiges nicht mehr funktioniert hat verschiedene Ursachen, die ich schon teiweise ermitteln konnte.

Prozessorspezifische Interrupt Eigenschaften, plötzlich "volatile" erforderlich, Code wegoptimiert.....

Ich bekomme sie sicherlich auch in jeder Optimierung zum Laufen, darum geht es nicht.

Ist das erforderlich ist meine Frage. Zudem könnte ich mir vorstellen, dass ein Compiler von beispielsweise IAR
eine anderen Assemblercode erzeugt als ein GNU Compiler, bei Optimierungen ebenfalls Unterschiede entstehen.

Im wesentlichen gibt es zwei, sich widersprechende, Optimierungen:
- Codegrösse
- Geschwindigkeit

Von daher müssen nie beide Optimierungen sein :-(
Grundsätzlich muss nicht optimiert werden, Codegrösse kann helfen, ein grosses Programm noch ins ROM zu bringen.

Grundsätzlich sollte der Code nicht kaputt optimiert werden, zumindest von der Ablauflogik her.
Das andere Problem ist das Laufzeitverhalten, hier ändert sich das Timing, was zu Problemen mit der Peripherie führen kann.

Von der Programmlogik kann eine leere for-Schlaufe wegoptimiert werden, da deren Entfernen nichts an der Ablauflogik ändert. Dumm nur wenn das ein Delay war :-(

Allerdings kann man die Optimierung auch meistens über #pragma im Code steuern, sodass man kritische Teile von der Optimierung ausnehmen kann.

Das mit dem "volatile"-Problem verstehe ich nicht so ganz? Da vermute ich, dass da grundsätzlich etwas nicht ganz sauber im Programm ist, aber erst beim Optimieren wirklich entdeckt wird.
Typisches Beispiel:

while (!Status)
{
// warten bis bereit
}

Ohne Optimierung wird Status bei jedem Durchgang neu geladen.
Bei der Optimierung auf Geschwindigkeit wird "Status" nur einmal geladen und in einem Register aufbewahrt, das spart Speicher-, bzw. Peripherie-Zugriffe.
Nun ist es aber so, dass man bei einer sauberen Programmierung z.B. I/Os IMMER als "volatile" deklariert!
"volatile" ist besonders wichtig bei der Optimierung und besagt dem Compiler, dass sich der Zustand dieser Variablen jederzeit ändern kann, auch wenn dies vom Programmablauf her nicht "sichtbar" ist.
Ein C-Compiler kennt den Unterschied zwischen einer normalen Variablen im RAM und einem Port nicht. Allerdings kann "volatile" auch wichtig sein, wenn eine Variable in einer ISR verändert wird.

MfG Peter(TOO)

shedepe
27.10.2015, 10:29
Man könnte auch behaupten, wenn man nach der Spezifikation programmiert klappt das.

Es ist nunmal so, dass unterschiedliche Compiler sich unterschiedlich verhalten. So lange man sich aber innerhalb der Spezifikationen der Programmiersprache - bzw. der Ergänzungen der Compiler aufhält, sollte man nicht befürchten, dass es Probleme hinsichtlich des entstehenden Binärcodes gibt.

Man muss sich eben davor einlesen, was die Toolchain bzw. die Sprachspezifikation z.B. zu Codeoptimierung sagt. Bzw. die Prinzipien dahinter verstehen.

Du forderst von deinem Compiler dass er Code Optimiert. Je nach Einstellung macht er das anders. Das musst du dir davor anschauen.
Die meisten Optimieren haben aber gemeinsam: Zuerst schaut der Compiler: gibt es unnötigen Code. Also Code der nicht aufgerufen wird. Wenn ja (das trifft für Interruptroutinen zu bzw. Variablen die dort verwendet werden). Aus Sicht des Compilers wird die Interruptroutine nie aufgerufen ( denn deine Hardware macht das). Also kann er gefahrlos die Variablen wegoptimieren (sie werden ja nicht benötigt) . Volatile gibt an: Nein die Variable wird gebraucht auch wenn du das nicht siehst.

Achtung außerdem. Volatile auf dem PC bedeutet etwas ganz anderes.

Peter(TOO)
27.10.2015, 10:45
Hallo,

Eine gewisse Überprüfung - allereinfachster Art - wäre natürlich die Angabe des Compilers über den verbrauchten Speicherplatz als Flash und SRAM (also Program und Data) bei verschiedenen Optimierungen zu vergleichen.
Moderne C-Compiler unterscheiden sich heute meistens nur noch bei der Optimierung, den Speichermodellen und den mitgelieferten Zusatz-Bibliotheken. Die Standard-Bibliotheken sind durch ANSI-C vorgegeben.


Andererseits - sehr geschachtelte Routinen und/oder reichlich ISR brauchen jede Menge Stack. Da gibts empfehlungen die oberen 20 oder mehr Prozent SRAM frei zu halten - ich habe auch schon Empfehlungen von bis zu 50 Prozent gelesen.

Allgemeine Empfehlungen sind Quatsch!
Wie du richtig erkannt hast, spielt die Verschachtelung eine grosse Rolle. Zum Stackbedarf des Hauptprogrammes, muss jederzeit auch noch genügend Platz für, allenfalls auch geschachtelte, Interrupts frei bleiben. Neben dem Hardwarebedingten Platz für die Rücksprungadresse und zu rettende Register, kommen aber auch noch die Auto-Variablen hinzu!
Hier macht dann die Anwendung und das können des Programmierers den Stackbedarf aus.


(M)EIN knappes, einfachs Fazit also: "verdächtige" oder eben mehrfach benutzte Variablen als volatile deklarieren erspart manchen Kummer.
Eigentlich sollte man da nicht mit raten dran gehen, sondern mit Wissen ;-)

Jede Variable, welche sich ausserhalb des gerade sichtbaren Codes ändern kann, MUSS als "volatile" deklariert werden!
Also Variablen, welche in Interrupts verändert werden und natürlich fast alles was zur Peripherie gehört. Logischerweise die Ports und Timer, aber auch Register welche Status-Bits enthalten.

MfG Peter(TOO)

P.S. Ich habe ganz früher einige einfache Compiler selbst entwickelt, einer war für die Verarbeitung von Verdrahtungslisten für eine Simulation. Eine Art Hochsprache kann man auch mit einem guten Macro-Assembler erstellen. Ende 80er habe für den 6301, ein 6801 Derivat, habe ich einen Optimizer geschrieben, welcher den Assembler-Code optimiert hat. Der Compiler machte viel zu viel unnötige Registertauschereien. Im Prinzip hatte dieser Compiler Übergabekonventionen zwischen den Code-Zeilen, so in der Art wie man das auch bei einem Funktionsaufruf hat, vereinfacht natürlich den Compiler.
Von etwa 1990 bis 2000 habe ich am IAR-Compiler für die Hitachi-H8 Familie mitgewirkt.

Siro
27.10.2015, 13:46
Ersteinmal vielen Dank für Eure Informationen.

Vorab: Ich muss glücklicherwiese nichts optimieren, weder aus Speichertechnischen noch aus Geschwindigkeitsgründen.

Durch verschiedene Optionen möchte ich jedoch im Vorfeld schon "kritischen Code" erkennen, um eine möglichst "saubere"
Software zu generieren.

Das "volatile" ist natürlich zwingend erforderlich zum Beispiel bei meiner "SystemTickcount" Variablen.

Meine Dummy Variable zum Lesen eines Registers um lediglich ein Interrupt Bit zu löschen muss dann auch "volatile" sein,
sonst optimiert mir der Compiler die Zeile gänzlich weg, frei nach dem Motto: Was Du nicht benutzt, brauchst Du auch nicht :-)

Kritisch ist generell das Löschen von Interrupbits beim Cortex-M3. Das sollte man tunlichst nicht in der letzten Zeile tun.
Das geht dann plötzlich nicht mehr und es wird ein Dauer oder doppelter Interrupt ausgelöst.

Zur Zeit hängt die Software jedoch immer noch fest und ich bin noch auf der Suche.

Siro
03.12.2015, 13:08
Ich möchte das Thema nochmal aufgreifen, da ich neue "merkwürdige" Erkenntnisse gewonnen habe:

Mein Code läuft jetzt mit Allen möglichen Optimierungen, das war ein gutes Stück Arbeit.
Ein Fehler hat wirklich lange gedauert und dieser ist mir auch bisher nicht erklärlich. Vielleicht habt Ihr eine Erklärung dafür:

Diese Zeile hat der Compiler wegoptimiert wenn *base nicht volatile ist */
das darf er doch nur, wenn er wüste, dass da schon 0x83 drin steht, dem ist aber nicht so
*(base + UART_LCR) = 0x83; /* DLAB = 1 , 8 bits, no Parity, 1 Stop bit */

Dazu ein Ausschnitt aus dem Code zum Initialisieren der seriellen Schnittstelle:



/*----------------------------------------------------------------------------*/
void uart_init(const U8 UartNo,const U32 baudrate)
{ volatile U8* base;

base = (U8*) UART_BASE[UartNo]; /* base address of uart, Basisadreesen sind im Array abgelegt */

*(base + UART_LCR) = 0x83; /* <---- Zeilenklau Compiler DLAB = 1 , 8 bits, no Parity, 1 Stop bit */

/* .............Werte für DLL und DLM setzen............. */

*(base + UART_LCR) = 0x03; /* <--- diese Zeile lasst der Compiler drin DLAB = 0 , 8 bits, no Parity, 1 Stop bit */

}




warum verschwindet die Zeile lediglich bei Optimierungsstufe -O3 ?

Siro

PS. Das ist LPCXpresso v7.9.2 [Build 493] [2015-09-14]
Gnu-Compiler C/C++ GNU Toolchain Build Support 8.6.0.201502131403 org.eclipse.cdt.gnu.build.feature.group Eclipse CDT

shedepe
03.12.2015, 13:23
Sofern ich das auf die Schnelle sehe sind bereits alle Werte bekannt.

Peter(TOO)
03.12.2015, 16:14
Hallo Siro,

Ist doch für einen Automaten logisch! :-=


a = 0x83;
a = 0x03;

Da steht am Schluss immer 0x03 in a!

Wie schon geschrieben wurde, ist das ohne volatile für den Compiler nur eine Variable im Speicher!

a = 0;
a = 1;
a = 2;
a = 3;
Da macht nur a =3; Sinn, die anderen Werte werden dauernd überschrieben ohne je gelesen zu werden.

Die CPU, und der Compiler, behandeln bei dieser Architektur Register und Speicher genau gleich, da gibt es keinen Unterschied.
Da muss man dann halt mit volatile "nachhelfen" und den Compiler beibringen, dass sich diese Speicherzellen auch ohne zutun des Programms änder können, bzw. das Setzen und anschliessende Löschen eines Bits etwas bewirken.

Es gibt Architekturen, bei denen I/Os am einem separaten Bus hängen, da kann das der Compiler dann an der Adresse unterscheiden. z.B. der 8080 und auch die Nachfolger 8086 bis Pentium haben so einen separaten I/O-Bus. Beim 8080 gab es 256 I/O-Adressen, beim 8086 waren es dann 65KByte. Natürlich kann man aber auch bei dieser Familie I/Os im normalen Speicherbereich einbinden.

MfG Peter(TOO)

Siro
03.12.2015, 21:00
Jetzt, wo Du es so erklärst, fällt auch bei mir der Groschen (Centweise ;))

Na klar, der Compiler sieht, dass ich einer Variablen einen Wert zuweise, nix damit mache und dann einen neuen Wert zuweise.
Er kann den Sinn natürlich nicht erkennen, dass ich dies tue um an spezielle Register im Uart heranzukommen.

Das leuchtet mir jetzt auch ein und damit hat er recht und darf das wegoptimieren.
Vielen Dank Peter

Peter(TOO)
04.12.2015, 07:43
Hallo,

Jetzt, wo Du es so erklärst, fällt auch bei mir der Groschen (Centweise ;))

Dann bist du jetzt reich ;-)

Es gibt noch weitere Optimierungen, das wird dann auch die Reihenfolge der Befehle umgestellt, z.B. um die FPU besser auszunutzen, kann man, während die FPU gerade rechnet noch ein paar Integer-Befehle abarbeiten. Das hängt dann aber sehr von der CPU-Architektur ab. Bei manchen RISC-CPUs geht das so weit, dass das Return einer Subroutine VOR dem letzten Befehl stehen muss!

Grundsätzlich muss man bei Funktionen, welche direkt I/Os bedienen mit der Optimierung sehr genau aufpassen! :Strom
Meistens ist es am Besten diese separat auszulagern und ohne Optimierung zu übersetzen.
Andernfalls kann schon eine neue Version des Compilers, mit verbesserter Optimierung, zu einem anderen Timing, und folglich zu Fehlern, führen.

Noch etwas grundsätzliches zur Funktion eines Compilers:
Zuerst wird eine lexikalische Prüfung vorgenommen, dabei wird im Prinzip die Rechtschreibung überprüft.
Anschliessend folgt die syntaktische Prüfung, bei welcher festgestellt wird ob aus den Wörtern auch gültige Sätze gebildet wurden.
Anschliessend bildet man eine Baumstruktur, welche den logischen Programmablauf darstellt und eigentlich Sprachunabhängig ist.
Diesen Sprachabhängigen Teil nennt man Front End.

An diesem Baum erfolgt dann die erste Optimierung, wie z.B. das wegoptimieren von unnötigen Variablen-Zugriffen und totem Code.

Der Code-Generator (Back End) erzeugt dann aus dem Baum den eigentlichen Maschinencode. Dieser Teil ist dann CPU spezifisch.
Hier wird dann weiter optimiert, vor allem da hin gehend, dass man viel benutzte Variablen möglichst in einem Register behält und erst am Ende der Berechnung in den Speicher schreibt (volatile verhindert auch diese Optimierung).
Weitere Optimierungen sind z.B. bei einer Multiplikation mit 2, das Ersetzen durch eine Addition oder einen Links-Shift, je nachdem was halt schneller ist.

MfG Peter(TOO)

Siro
13.12.2015, 15:36
Hallo Peter,
ich wollt mich nochmal bedanken, für die zusätzlichen Informationen.

Ich habe tatsächlich noch ein Problem gefunden. Ich habe eine "Inkontinenz" :) Inkonsistenz in meinem Code gefunden.
Im Header habe ich einen 32 Bit und in C-Code Modul einen 16 Bit Wert declariert/definiert. Das merkt der Compiler nicht und beim IAR-Compiler wird funktioneller Code erzeugt
der Gnu-Compiler hingegen erzeugt nicht funktionellen Code. Eindeutig meine Schuld :p, aber so unterschiedlich kann der erzeugte Code durch einen anderen Compiler halt werden.

Obwohl die Sourcen im Projekt zur Verfügung stehen, guckt der Compiler sie sich nicht an und damit gibt es auch kein Warning. Schade eigentlich.


Datei: FlowCalc.h
extern volatile U32 flowfactor;

Datei: FlowCalc.c
U16 flowfactor = 256;

mir fällt jetzt auch nichts ein, wie man solche Fehler vermeiden kann.

botty
13.12.2015, 18:09
Hallo Sisor,

kann es sein, dass Du FlowCalc.h nicht in FlowCalc.c inkludierst?

Also der GCC schreit normalerweise (oder spätestens bei -Wall) wenn eine Variable zuvor anders deklariert wurde, als sie dann später definiert wird.

Gruss
Chris

Siro
13.12.2015, 18:18
kann es sein, dass Du FlowCalc.h nicht in FlowCalc.c inkludierst?
Chris

Das kann nicht nur sein, sondern das ist so....

Wenn ich aus einer H-Datei nix brauche, includiere ich sie niemals. Sollte man das tun ?

Vielen Dank für den Hinweis Chris

botty
13.12.2015, 18:29
Was man immer macht, ist, dass man eine Include-Datei, in die zugehörige C-Datei inkludiert.
In Deinem Beispiel also FlowCalc.h in FlowCalc.c, eben genau um solche Fehler aufzudecken (Ich selbst habe den IAR-Compiler noch nie benutzt, aber auch der sollte dann meckern.).

Ausserdem schreibt man in eine H-Datei immer Guards, für den Fall das man aus Versehen sie mehrmals inkludiert - was ohne Guards wieder zu Fehlermeldungen führt.

Also fuer FlowCalc.h:



#ifndef _FlowCalc_h_
#define _FlowCalc_h_

/* Hier kommen jetzt alle defines, Deklarationen etc. */

#endif

Siro
13.12.2015, 18:42
Was man immer macht, ist, dass man eine Include-Datei, in die zugehörige C-Datei inkludiert.

Das habe ich bisher nur gemacht, wenn es wirklich nötig war und dann habe ich die entsprechenden "Guards" gesetzt.
Wuste garnicht, dass die so heissen.

Okay, dann werde ich meinen Stil ab jetzt ändern, damit der Compiler mögliche Konflikte auch aufdecken kann.
Man lernt nie aus. Ich danke Dir.

Siro

Peter(TOO)
13.12.2015, 19:00
Hallo Chris,

Das kann nicht nur sein, sondern das ist so....

Wenn ich aus einer H-Datei nix brauche, includiere ich sie niemals. Sollte man das tun ?

Du musst das so betrachten.

1. Man schreibt irgendein Modul, welches teilweise Funktionen bereit stellt, welche aus anderen Modulen benutzt werden können.
2. Man schreibt zu diesem Modul eine Header-Datei, welche alles enthält, was zum externen Aufruf benötigt wird. Also natürlich die Funktionen, aber auch öffentliche Konstanten und Variablen-Konstrukte.
3. Überall wo dann dieses Modul verwendet werden soll, wird die Header-Datei includiert.
4. Damit der Compiler auch überprüfen kann, dass Header und Modul zusammenpassen, includiert man diese auch im Modul!
5. Alle Funktionen welche in dem Modul nicht öffentlich sein sollen, declariert man als static, dann kann diese auch der Linker nicht finden und man darf die selben Namen auch in einem anderen Modul verwenden.

So wird der Code auch für andere übersichtlicher, besonders wenn man etwas anpassen muss:
Alle Funktionen und Variablen welche als static deklariert sind, kann man problemlos anpassen ohne auf Aufrufe aus anderen Modulen acht geben zu müssen.
Alles was öffentlich verwendet wird ist in der Header-Datei und der Compiler kann überprüfen ob es passt.

MfG Peter(TOO)