Hallo zusammen,
ich weiß, ich bin etwas spät dran, aber möchte Euch vor Abgabeschluss
noch nach etwas Feedback zu meinem Artikel fragen:
http://www.mikrocontroller.net/articles/Plattformunabh%C3%A4ngige_Programmierung_in_C
Johann L. hat mir auf der Diskussionsseite schon einige Punkte
geschrieben (Vielen Dank dafür!), die ich größtenteils eingearbeitet
habe. Aber vielleicht gibt es ja noch weitere Verbesserungsvorschläge
oder Aspekte, die noch erwähnt werden sollten.
Insbesondere ist mir wichtig, dass im Artikel keine groben
Falschaussagen stehen. Ich habe zwar nach bestem Gewissen recherchiert,
bin aber natürlich nicht unfehlbar. Ich würde mich daher sehr freuen,
wenn die C-Experten mal einen Blick drauf werfen könnten.
Aber auch für allgemeines Feedback von jedem Interessierten wäre ich
sehr dankbar: Ist alles verständlich formuliert, ist etwas zu kurz oder
zu ausführlich? Vermisst Ihr irgendwo Erklärungen oder Links?
Ans Ende kommt noch ein kurzes Kapitel, wie man plattformabhängigen und
plattformunabhängigen Code voneinander trennen kann und eine
Zusammenfassung mit den wichtigsten Empfehlungen. Das schreibe ich
morgen ...
Ansonsten ist es inhaltlich aus meiner Sicht jetzt ziemlich vollständig.
Also, vielen Dank schon mal fürs Lesen. :)
Fabian O. schrieb:> Also, vielen Dank schon mal fürs Lesen. :)
Hallo,
ich sehe das ganze eher als Nachschlagewerk
und muß gestehen daß ich es nur überflogen habe.
Das ist wahrscheinlich einer der umfangreichsten Artikel im Wettbewerb.
Was ich noch vermisse sind so die typischen Fallstricke von C wie z.B.
das rechts-shiften von signed Größen.
Aber möglicherweise ist das ja im Kapitel Kapselung dann enthalten.
Gruß Anja
was ich nicht schön finde sind diese Codefragmente als Beispiele - okay
man kann sich denken was man dazu basteln muß um ein lauffähiges
Programm zu bekommen und das ist ja wohl mittlerweile gängige Praxis in
der Erklärung, ich persönlich hasse es trotzdem.
Du solltest wenigstens ein lauffähiges Beispiel bringen, sonst wird es
für Anfänger schwierig.
Compilierungsvorgang für unterschiedliche Platformen wäre auch noch eine
nette Ergänzung.
Ergänzen könnte man auch noch einige andere Sachen, aber das würde den
Rahmen sprengen - insgesamt ganz okay.
noch was: ich weiß nicht wer Deine Zielgruppe ist - der erfahrene
Programmierer, der Anfänger, beide?
Wenn Du Fachtermini einführst, sollten die immer kurz erklärt werden,
was Du nur teilweise machst (z.B. Literale war okay).
Was ist ein Cast, eine Union, etc.?
Die Unterschiede zwischen den Standards und den Möglichkeiten von C99
hast Du gut erklärt, dafür ein Lob ;-)
Danke für Eure Antworten.
Anja schrieb:> ich sehe das ganze eher als Nachschlagewerk> und muß gestehen daß ich es nur überflogen habe.> Das ist wahrscheinlich einer der umfangreichsten Artikel im Wettbewerb.
Ja, war aber eigentlich gar nicht mein Ziel ... Bin selber erstaunt, wie
viel da zusammengekommen ist. Aber wenn man mal anfängt, diese ganzen
Sachen aufzuschreiben, fallen einem eben doch eine Menge Dinge ein, die
man erwähnen sollte. Deshalb kommt noch die Zusammenfassung ans Ende,
dass man das wichtigste noch mal als kompakten Überblick/Erinnerung hat.
> Was ich noch vermisse sind so die typischen Fallstricke von C wie z.B.> das rechts-shiften von signed Größen.
Stimmt, dem Thema signed/unsigned könnte man noch ein paar Sätze
spendieren. Allerdings soll der Artikel auch nicht jeden möglichen
Programmierfehler, der zu undefiniertem Verhalten führt, auflisten (i =
i++ etc.), sondern nur die Dinge, die plattformspezifische Gründe haben.
Sonst wird das noch viel viel länger ... ;)
Passwort vergessen schrieb:> noch was: ich weiß nicht wer Deine Zielgruppe ist - der erfahrene> Programmierer, der Anfänger, beide?
Ich glaube das ist der Punkt: Der Artikel ist kein C-Tutorial für
Anfänger, auch wenn es ganz am Anfang vielleicht so klingt (Erklärung
der Integer-Typen). Er richtet sich an Programmierer, die schon C
können, aber sich noch nicht mit plattformspezifischen Eigenheiten und
Portierung von Code beschäftigt haben, sondern bisher immer zufrieden
waren, wenn der Code auf ihrem Mikrocontroller funktioniert hat.
Deshalb setze ich voraus, dass der Leser grundsätzlich weiß, wie Shifts,
Casts, Zeiger, Unions usw. funktionieren. Gleiches gilt für die
Beispiele: Ich setze voraus, dass der Leser schon den Kenntnisstand hat,
sich vorzustellen, wie sie in einem realen Programm aussehen. Werde das
am besten am Anfang des Artikels noch klarer erwähnen.
Ist vielleicht der Titel "Plattformunabhängige Programmierung in C"
missverständlich? Also à la "Programmierung in C (unabhängig von einer
Plattform erklärt)"? Denn das ist nicht gemeint, sondern es geht um
plattformunabhängigen Code, nicht wie man C programmiert. Hmmm ... hat
jemand einen besseren Vorschlag für den Titel?
Auf jeden Fall Danke, dass Du das angesprochen hast. :)
Fabian O. schrieb:> etwas Feedback zu meinem Artikel...
Nun ja, dein Ziel ist m.E. ein bissel zu hoch gesteckt:
"Ziel ist es, Softwaremodule ohne Änderungen auf möglichst jeder
Hardwareplattform einsetzen zu können, vom 8-Bit-Mikrocontroller bis hin
zum 64-Bit-PC."
Wir reden hier von Mikrocontroller-Anwendungen, ja? Die sind eigentlich
fast immer so hardwarenah, daß die simple formale Übersetzbarkeit von
Quellen eher in den Hintergrund tritt. Viel wichtiger ist es, die zu
einem Projekt gehörigen Quellteile thematisch und inhaltlich so zu
gruppieren und mit sinnvollen Schnittstellen zu versehen, daß sich
daraus eine Weiterverwendbarkeit diverser Teile ergibt. Leider sieht man
auch in diesem Forum Unmengen von Quellcode, der das ziemliche Gegenteil
darstellt, obwohl deren Autoren allen "modernen" Schnickschnack a la
uint16_t usw. benutzt haben und nun glauben, ihr Code sei damit bestens
portabel.
Noch ein Wort zu C99 und zu den Unsäglichkeiten, die damit aufgekommen
sind, insbesondere zu den Integer-Verrenkungen: An der eigentlichen
Sprache ist damals überhaupt nix geändert worden. Die Compiler sehen
nach wie vor nix anderes als char, int, long und evtl. short sowie
eventuellen Präfixen. All die "neuen" Typen wie uint16_t und so sind
keine echten neuen Typen, sondern lediglich Header-Akrobatik und damit
genauso gut oder schlecht wie Typen a la U8, S16, U32 oder sonstwelche
Eigendefinitionen, die sich jeder selbst nach eigenem Geschmack anlegen
kann. Das sollte man mal wirklich klarstellen.
Was die portable Benutzung von char, int, short, long und int64 aka long
long betrifft, so hast du die bereits implizit beschrieben. Man muß
deine Tabelle lediglich mal als Logiker betrachten. Huch? Ja. Also die
da stehende Aussage lautet z.B. "der C Standard garantiert lediglich
folgende Mindestbreiten.." z.B. für int 16 Bit. Das heißt im logischen
Umkehrschluß und etwas anders formuliert so:
"Ein int ist garantiert 16 Bit breit und man darf sich keinesfalls
darauf verlassen, daß der int auch nur ein einziges lumpiges Bit größer
ist als 16 Bit. Für den Programmierer ist somit der int also exakt 16
Bit breit."
Das gilt auch dann, wenn auf manchen Maschinen in der CPU automatisch
breiter gerechnet wird, wie das z.B. bei ARM-Maschinen der Fall ist. Was
in diesem Zusammenhang viel wichtiger ist, ist der Umstand, daß manche
Maschinen ausgerichtete ("aligned") Daten benötigen und andere nicht.
Das schafft beim Portieren von Strukturen viel größere Probleme als das
alberne Herumgehacke auf den Integer-Bezeichnungen. Ähnlich sieht es mit
Little und Big Endian Maschinen aus.
Das sind alles viel delikatere Portierungsprobleme. Dein "Einer der
fundamentalsten Unterschiede zwischen.." muß man also etwas
relativieren.
Ebenso muß man den Passus über Pointerdifferenzen relativieren: Die
schiere Bitanzahl ist ja nicht unwichtig, aber gerade bei
Mikrocontrollern haben wir sehr oft ganz andere Probleme damit, daß es
Adreßraum-Inkompatibilitäten gibt. Bei vielen Maschinen muß man
unterscheiden zwischen Pointern, die auf R/W-Daten zeigen und solchen,
die auf R/O Daten zeigen, Stichwort Harvard versus v.Neumann. Da können
2 Pointer numerisch völlig gleich sein und sachlich doch völlig
unterschiedlich.
Zum Schluß noch ein Wort zum Casten: " Klarheit kann man, wie vorher,
über einen Cast schaffen:... "
Nun, im Prinzip kann man es so tun, aber die meisten Leute casten falsch
mangels Vorstellung der inneren Logik - und es sieht das Herumgecaste
auch unästhetisch und unleserlich aus.
Beispiel: MyLong = (long)(MyByte<<8) | MyotherByte; --->ätschibäh!
Man sollte m.E. eher empfehlen, möglichst ohne casts auszukommen und
die Optimierung dem Compiler zu überlassen:
MyLong = MyByte; MyLong = (MyLong<<8) | MyotherByte;
Funktioniert immer, braucht keine casts und gibt bei ordentlichem
Compiler nen guten Code.
so, das waren erstmal meine Anmerkungen dazu.
W.S.
Guter Artikel!
Ich würde noch auf den Aufbau der Software in Abstraktionsebenen
eingehen, mit Verweis auf das OSI-Modell der Netzwerkschichten als gutem
Beispiel.
W.S. schrieb:> Für den Programmierer ist somit der int also exakt 16 Bit breit."
Das wär nun aber völlig daneben, da es sehr wohl grösser sein kann. Ob
etwas in einer Datenstruktur nun 2 oder 4 Bytes beansprucht ist nicht
immer völlig unwichtig.
Deine gediegene aber etwas einsame Aversion gegen int16_t&Co ist an
anderer Stelle schon eingehend zur Sprache gekommen.
Bronco schrieb:> Ich würde noch auf den Aufbau der Software in Abstraktionsebenen> eingehen, mit Verweis auf das OSI-Modell der Netzwerkschichten als gutem> Beispiel.
Wobei ich das eher als separaten Artikel sehe, da sich der Artikel auf
Programmierung in C konzentriert, die Vorteile von Programmierung in
Abstraktionsebenen aber - in schönster Abstraktion - völlig unabhängig
von der Programmiersprache gelten.
Bei dem Tital "Plattformunabhängige Programmierung in C" wäre zunächst
mal zu erläutern, was unter einer "Plattform" zu verstehen ist.
Der Artikel geht auf unterschiedliche Sprachversionen (C90, C99, C11,
...) ein, aber unter einer "Plattform" wird wohl mehr verstenden,
nämlich auch implementation-defined Behavior", d.h. Dinge, die im
C-Standard zwar erwähnt sind, aber deren Definition an die
Implementation (Compiler, Linker, LibC, Host-OS, ...) delegiert werden.
Dazu gehören zB die Anzahl signifikanter Zeichen in Identifiern und
Dateinamen, sizeof (int), etc.
Im GCC-Manual gibt es einen eigenen Abschnitt zu "implementation
defined" und was GCC definiert. I.W. delegiert GCC die Definition
weiter: An die LibC, an die Binutils, an eine (E)ABI.
Anstatt direkt in medias res zu gehen, wäre ein Überblick zweckmäßig so
daß man sich nicht direkt in den Details verliert. Das Integer-Thema
ist recht ausgewachsen, vielleicht würde ihm eine Straffung guttun so
daß man nicht den Überblick verliert...
Beim Thema "Plattformunabhängigkeit" wäre auch ein Hinweis zu den Teilen
eines Programms angebracht, die nicht portierbar sind. Auch hier wäre
ein kurzer Überblick sinnvoll, wie man diese nicht-portierbaren Teile
möglichst praktikabel rausfaktorisiert. Wie erkennt man zB die
Plattform? Das Device? Die Architektur? Das Host-OS? Wie erkennt man die
Sprachversion in der Quelle? Wie sollte man zB ISR-Implementierungen
"Kapseln" oder SFR-Zugriffe.
Dies alles ist inheränt nicht-portierbar, d.h. die Kunst bei der
Implementierung besteht in einer sauberen Trennung des portierbaren
Teils und des nichtportierbaren Teils. Hier sind auch Aspekte der
Effizient zumindest zu erwähnen.
Die Frage ist auch, wie praxisrelevant eine komplette Portierbarkeit
zwischen einem 64-Bit und einem 8-Bit System überhaupt ist. Zwar gibt
es zurzeit sowohl kleine 8-Bit µC als auch bereits homogene und
heterogene Multicores (Freescale/ST PowerPC VLE, Infineon Aurix, ...)
aber die Notwendigkeit einer Portierung in maximal vorstellbarer
Bandbreite dürfte eher akademisch sein...
volatile + Bitfelder: Das ist fürchterlich, weil es nicht spezifiziert
ist und auch kaum spezifiziert werden kann. Der C-Standard macht keine
Aussage (weil er den Instruktionssatz nicht kennt) und der Compiler ist
relativ frei, wie er das umsetzt. Zb zwei volatile Bitfelder
nebeneinander: Wenn ein Bitfeld zu ändern ist und es keine
Bitinstruktion gibt, müssen zwangsläufig andere Felder
mitgelesen/mitgeschrieben werden.
Ergo: Funger weg von volatile + Bitfelder, selbst wenn es um
Devive-spezifische Header geht wie sie Atmel z.B. for XMEGA
bereitstellt. Das kann man zur Kenntnis nehmen, aber in dem Artikel
würde ich nicht groß auf diese Schlangengrube eingehen.
Johann L. schrieb:> Die Frage ist auch, wie praxisrelevant eine komplette Portierbarkeit> zwischen einem 64-Bit und einem 8-Bit System überhaupt ist.
Da die Portierbarkeit zwischen 8-, 16- und 32-Bit Architekturen ein
ziemlich aktuelles Thema ist, kommt es auf eine Vervollständigung durch
64-Bitter nun auch nicht mehr an. Vom Adressraum abgesehen sind die dank
dominierendem LP64/LLP64 auch nur falsche 32-Bitter, die besser mit
64-Bit Typen umgehen können als echte 32-Bitter.
Vielen Dank für die Kommentare. :)
Ich habe noch das Kapitel über die Aufteilung von Code geschrieben. Ist
durch den vielen Quellcode etwas lang, aber es ist ja das letzte
Kapitel. Wen es nicht interessiert, braucht also nicht mehr weiterlesen.
Oder soll ich versuchen, es zu kürzen?
Ansonsten werde ich schaun, dass ich morgen noch eure Vorschläge einbaue
hinsichtlich Überblick am Anfang geben etc. Falls ihr noch weitere
Anregungen habt, es sind ja noch 24 Stunden Zeit ... :D
Fabian O. schrieb:> Ich habe noch das Kapitel über die Aufteilung von Code geschrieben.
Schau dir die Codebeispiele noch mal an, und zwar die Reihenfolge der
#include-Statements.
Beispiel:
rfm70.h wird vor stdint.h eingebunden, enthält aber eine Deklaration
mit einem stdint-Datentyp.
Grüße
Stefan
PS: Ansonsten macht der Artikel einen guten Eindruck.
Es gibt keine "Fließkommazahlen", sondern nur "Gleitkommazahlen", auch
wenn man der Mist oft liest...
Zur Info:
float = gleiten, schwimmen, schweben, ...
flow = fließen, strömen, ...
Wer "Fließkommanzahl" verwendet, übersetzt auch "bekommen" mit
"become"...
Ok, noch ein letztes Feedback :-)
Meterlangen Beispielcode finde ich persönlich nicht so hilfreich, der
ist eher dazu angetan sich im Urwald zu verlieren und vom Hölzchen aufs
Stöckchen zu kommen...
Ironischerweise ist der Code nicht gut portabel, für eine Arraylänge
wird ein uint8_t verwendet, was aber ein size_t sein sollte. Und es
werden C++ Kommentare verwendet, die es erst in C99 gibt.
Daß bestimmte Compiler Konstrukte zum Packen (packed) oder zum Alignment
(aligned, assume_aligned, alignedaccess, ...) haben, kann durchaus
Erwähnung finden, darin baden würd ich aber nicht. Eher angebracht sind
hier ein paar Worte zu Serialisierung / Deserialisierung und wie diese
abgebildet werden konnen. Über die binärdarstellung der entsprechenden
Objekte zu gehen ist nur eine Möglichkeit; (De)serislisierung per ASCII
ist eine weitere.
Anstatt sich weiter in Codeschnippeln zu verlieren, wäre eine Abrundung
des Artikels ein Blick auf die Portierbarkeit des von den Tools
erzeugten Binärcodes.
Auf Host-Rechnern ist es zB unerlässlich, daß Code unterschiedlicher
COmpilerhersteller (Microsoft, GCC, Intel, HP, ...) interoperabel ist,
d.h. man kann zB mit GCC eine Anwendung erstellen, die zusammen mit DLLs
von Microsoft operiert.
Hierzu muss natürlich das ABI passen, etwa Objektformat, Calling
Convention, Linkage (C++ Demangling), etc.
Beim Thema "pached" von oben geht es genau um diese Binärkompatibilität,
es geht nicht um "Quellkompatibilität".
Unter den AVR-Compilern ist es zB nicht so, daß man einfach Objekte
(*.o) oder Bibliotheken (*.a) unterschiedlicher Hersteller mixen könnte.
Selbst innerhalb der gleichen Tools können sich Inkompatibilitäten
ergeben, etwa beim Wechsel von avr-gcc 4.6 zu avr-gcc 4.7.
Ein Hinweise und Anmerkungen dazu, um zumindest ein Problembewusstsein
dafür zu schaffen, fände ich angebracht.
So geht es an vielen anderen Stellen des Artikela auch eher darum, die
viele Fallstricke übersichtlich und in nicht-ermüdender Weise
darzulegen, als sich im Klein-Klein zu verlieren.
>Ironischerweise ist der Code nicht gut portabel, für eine Arraylänge>wird ein uint8_t verwendet, was aber ein size_t sein sollte
Warum sollte man uint8_t nicht verwenden können?
Habe mir den Text flüchtig angesehen und dabei folgendes gesehen:
Die interne Repräsentation von Fließkommazahlen, d.h. wie die einzelnen
Bits bzw. Bytes im Speicher angeordnet sind, ist allerdings
plattformabhängig! Die Typen float und double sind deshalb nicht für den
Datenaustausch zwischen verschiedenen Geräten, sondern nur für interne
Berechnungen geeignet. Wenn Fließkommazahlen über einen Datenbus bzw.
Netzwerk übertragen werden sollen, empfiehlt sich die Wandlung in
Festkommazahlen oder Strings.
Ich war bisher der Meinung, dass C-Compiler das IEEE_format verwenden
und keinesfalls die bits irgendwie unterschiedlich verwenden. Dafür
wurde doch das IEEE-Format geschaffen. Höchstens die endien-ness kann
verschieden sein.
Unterschiedliche Formate von NAN oder INFINITY löst man aber nicht mit
Wandlung in Festkommazahlen, auch hat eine Wandlung in Strings
erhebliche Laufzeit- und Performance-Nachteile.
Eine Darstellung was wirklich unterschiedlich sein könnte, wäre sehr
hilfreich.
chris schrieb:>>Ironischerweise ist der Code nicht gut portabel, für eine Arraylänge>>wird ein uint8_t verwendet, was aber ein size_t sein sollte>> Warum sollte man uint8_t nicht verwenden können?
In dem Zusammenhang kann man es schon verwenden, und meine Kritik geht
eher in Richtung defensive Programmierund als Portabilität.
Wenn man nämlich den Puffer vergrößert fliegt einem alles um die Ohren
:-)
@Fritz
Wikipedia sagt dazu:
> Die Anordnung der Bits einer single zeigt die nachfolgende Abbildung. Die> bei einer Rechenanlage konkrete Anordnung der Bits im Speicher kann von> diesem Bild abweichen und hängt von der jeweiligen Bytereihenfolge> (little/big endian) und weiteren Rechnereigenheiten ab.http://de.wikipedia.org/wiki/IEEE_754#Zahlenformate_und_andere_Festlegungen_des_IEEE-754-Standards
Im C-Standard ist es auf jeden Fall nicht definiert, da stehen nur die
geforderten Mindestwertebereiche (die auch nicht unbedingt jeder
Compiler einhält, siehe avr-gcc).
Mir fällt keine "legale" und effiziente Möglichkeit ein, an die korrekte
Bitrepräsentation gemäß IEEE zu kommen. Es garantiert einem ja niemand,
dass die gleiche Bytereihenfolge wie bei Integern verwendet wird, man
also einen float in uint32_t casten kann und dann byteweise verschicken.
Oder steht das im IEEE-Standard?
Super, endlich wird mal etwas zur Integer Promotion geschrieben. Danke!
Etwas härter hätte ich den Text bei den "union"s formuliert. In den
einen member etwas hineinzuschreiben und aus dem anderen wieder
herauszulesen ist grober Unfug (auch wenn das in der Vergangenheit
manche Programmier "cool" fanden). Tatsächlich genügt schon der Wechsel
zu einem Prozessor mit einer anderen byte-Anordnung um ein
"unerwartetes" Ergebnis zu bekommen.
Oliver
Fabian O. schrieb:> @Fritz> Wikipedia sagt dazu:>>> Die Anordnung der Bits einer single zeigt die nachfolgende Abbildung. Die>> bei einer Rechenanlage konkrete Anordnung der Bits im Speicher kann von>> diesem Bild abweichen und hängt von der jeweiligen Bytereihenfolge>> (little/big endian) und weiteren Rechnereigenheiten ab.>> Im C-Standard ist es auf jeden Fall nicht definiert, da stehen nur die> geforderten Mindestwertebereiche (die auch nicht unbedingt jeder> Compiler einhält, siehe avr-gcc).>> Mir fällt keine "legale" und effiziente Möglichkeit ein, an die korrekte> Bitrepräsentation gemäß IEEE zu kommen. Es garantiert einem ja niemand,> dass die gleiche Bytereihenfolge wie bei Integern verwendet wird, man> also einen float in uint32_t casten kann und dann byteweise verschicken.> Oder steht das im IEEE-Standard?
Nein, ein Cast geht auf keinen Fall, weil das den Wert verändert.
Der einzige Standard-konfirme Weg an die Bits eines Fließkomma-Wertes zu
kommen ist memcpy.
Hilfreich beim (De)serialisieren sind ldexp, frexp, fpclassify etc.
Je nach Compiler gibt's auch Makros, die Typlayouts beschreiben. Im GCC
etwa
1
$ echo | gcc -E -dM -x c - | grep FLT
2
#define __FLT_MIN__ 1.17549435e-38F
3
#define __FLT_EVAL_METHOD__ 2
4
#define __FLT_EPSILON__ 1.19209290e-7F
5
#define __FLT_HAS_DENORM__ 1
6
#define __FLT_MIN_EXP__ (-125)
7
#define __FLT_MANT_DIG__ 24
8
#define __FLT_RADIX__ 2
9
#define __FLT_HAS_QUIET_NAN__ 1
10
#define __FLT_MAX_10_EXP__ 38
11
#define __FLT_HAS_INFINITY__ 1
12
#define __FLT_DIG__ 6
13
#define __FLT_MAX_EXP__ 128
14
#define __FLT_DENORM_MIN__ 1.40129846e-45F
15
#define __FLT_MAX__ 3.40282347e+38F
16
#define __FLT_MIN_10_EXP__ (-37)
die aber auch nicht portabel sind, bzw. nur innerhalb verschiedener
GCC-Inkarnationen.
GCC kennt übrigens mindestens 4 Arten von Endianess:
• Byte-Endianess: Innerhalb eines Word
• Word-Endianess: zB innerhalb eines 64-Bit Werts
• Bit-Endianess: Bitfelder
• float-Endianess
Daneben ist die Endianess / Alignment in unterschiedlichen
Speicherklassen zu unterscheiden wie: Stack, Register, static Storage,
etc. IdR stimmen diese überein, können sich aber durchaus
unterscheiden, zB wenn ein Wert auf den Stack gelegt wird (avr-gcc wegen
byteweisem Push und weil der Stapel nach unten hin wächst).
Für den Artikel geht das aber IMO zu sehr ins Detail; Quintessenz ist
daß aufgrund der Unterschiede eine 100% plattformübergreifende Lösung
kaum praktikabel ist.
Zweckmäßiger erscheint es da, die Abhängigkeit explizit zu
faktorisieren, z.B.
1
#if defined (__GNUC__) && defined (__AVR__)
2
/* avr-gcc */
3
#elif defined (__GNU__) && defined (__i386__) && __SIZEOF_DOUBLE__ == 4
4
/*GCCaufx86mit-fshort-double
5
#else
6
#error Platform not handled yet. Feel free to contribute
7
#endif
Allerdings gibt's auch da Probleme, etwa weil sich LLVM als GCC
ausweist, d.h. __GNUC__ definiert...
Aber selbst wenn es nicht um Datenaustausch geht bleiben noch genügend
arithmetische Probleme mit float et al. Da man keine == verwenden soll
— etwa als Abbruchbedingung für iterative Algorithmen — stellt sich die
Frage, wie dies unabhängig von der Plattform und der Genauigkeit bzw.
dem Wertebereich zu formulieren ist.
Übrigens hat man Portierungsprobleme nicht nur im "reinen" C-Code,
sondern auch im Präprozessor. Hier können zB C99-Makros wie UINT32_C
verwendet werden, die laut Standard in #if verwendet werden können —
auch wenn manche libc-Implementierungen wie die AVR-LibC dem Standard
nicht folgen indem sie die Makros als Cast bereitstellen, was natürlich
nicht in #if verwendbar ist (Bug #36571).
Noch eins: Wegen eines nicht-vorhandenen C99-Compilers Teile in C++ zu
schreiben ist übel.
- C++ ist keine Obermenge von C
- C++ han andere Linkage, z.B Name-Mangling
Besser: Schnickschnack wie Mixen von Deklaration und Befehlen vermeiden
oder auf C90 rückportieren. Jede solche Mixtur kann durch Umstellung in
gültiges C90 umgeschrieben werden.
Hmm, Geschmackssache. Mich würde es ziemlich hart treffen, wenn ich
Schleifenzähler nicht mehr in for-Schleifen deklarieren dürfte. Mag
Schnickschnack sein, aber es macht den Code um einiges besser lesbarer,
genau wie Kommentare mit zwei Schrägstrichen.
Da achte ich lieber drauf, dass mein C-Code auch C++-konform ist und
passe ihn ggf. entsprechend an. Zugegebenermaßen habe ich bisher nicht
darauf geachtet, weils doch recht unwahrscheinlich ist, dass mein
AVR-Code mal unter Windows laufen soll. Aber die meisten
Inkompatibilitäten zwischen C und C++ sehen mir recht harmlos aus bzw.
widersprechen sowie ordentlich strukturiertem Code (z.B. Funktionen ohne
Prototyp aufrufen).
Ich habe mal ein "zur Not" in den Satz eingefügt. Sofern der Code sich
ohne große Änderungen in C++ kompilieren lässt, erscheint es mir
jedenfalls als das kleineres Übel, den C++-Compiler zu verwenden,
anstatt tausende Zeilen Quelltext auf "schlechter lesbar" umzuschreiben
...
Johann L. schrieb:>> Mir fällt keine "legale" und effiziente Möglichkeit ein, an die korrekte>> Bitrepräsentation gemäß IEEE zu kommen. Es garantiert einem ja niemand,>> dass die gleiche Bytereihenfolge wie bei Integern verwendet wird, man>> also einen float in uint32_t casten kann und dann byteweise verschicken.>> Oder steht das im IEEE-Standard?>> Nein, ein Cast geht auf keinen Fall, weil das den Wert verändert.
Hallo nocheinmal.
Vielleicht bin ich zu blauäugig oder pragmatisch. Verstehe die diversen
Warnungen nicht wirklich.
Habe mit einem STM32F4.. also mit FPU gerade eine Datenübertragung über
ein UART programmiert, wobei ich das IEEE_format auf 6 Bytes aufteile
und im PC wieder zusammensetze. Am PC in ein 32-bit integer und danach
auf float gecastet. Im STM laut IEEE per Asssembler zerlegt und
byteweise versandt. Ließe sich aber auch äquivalent in C durchführen.
Natürlich geht ein direktes casten int -> float nicht, aber sehrwohl ein
casting über pointer:
float fl;
int32 data32;
fl = *((float*)&data32);
Ja, so hatte ichs auch gemeint, war schlecht formuliert. Aber das bricht
die Strict-Aliasing-Regel, steht auch im Artikel. Der Compiler darf Dir
solche Konstrukte in C99 ggf. wegoptimieren. Wirklich legal ist es nur
per memcpy().
Aber auch dann hast Du zwar die Bits des float, aber in relativ
"willkürlicher" Reihenfolge. Das heißt, es hängt von den erwähnten
Word/Byte/Bit-Reihenfolge der Plattform ab, wo welches Bit steht. Das
kann prinzipiell auf jedem Prozessor anders aussehen.
Das heißt natürlich nicht, dass diese Vorgehensweise nie funktioniert.
Wenn die Floats bei beiden Prozessoren mit der gleichen
Bitrepräsentation im Speicher stehen, dann klappt es schon, keine Frage.
Nur kannst Du Dich eben nicht drauf verlassen, dass sich der Code auch
auf anderen Plattformen so verhält. -> Er ist nicht portabel.
Das ist ja genau der Punkt bei dem Artikel: Keines der Codebeispiele
darin ist ein Fehler in dem Sinne, dass das nie funktioniert. Auf
manchen Plattformen läuft das wunderbar. Nur wenn man dann mal den
Compiler, Prozessor oder Betriebssystem wechselt, geht plötzlich nichts
mehr und die Fehlersuche beginnt ...
Auch nicht korrekt wegen Aliasing-Rules von C denn Integer und Floating
sind nicht kompatibel.
D.h. es kann davon ausgegangen werden, daß die Änderung eines Types
keine Änderung eines nicht-kompatiblen Types bewirkt.
Es bedeutet zwar nicht, daß dies zu problemen führen muß aber es macht
eben den Unterschied zwischen "robust bzw. standardkonfirm" und "Hack,
der in diesem Kontext funktioniert".
Fabian O. schrieb:> Aber auch dann hast Du zwar die Bits des float, aber in relativ> "willkürlicher" Reihenfolge. Das heißt, es hängt von den erwähnten> Word/Byte/Bit-Reihenfolge der Plattform ab, wo welches Bit steht. Das> kann prinzipiell auf jedem Prozessor anders aussehen.
Also Word/Byte-Reihenfolge verstehe ich ja, dass sie je nach Plattform
verschieden sein kann.
Aber Bit-Reihenfolge im Byte ist meiner Meinung nach wohl in allen
Plattformen gleich. Sich da nicht daraf verlassen zu wollen ist etwas
übertrieben.
4-bit uC`s gibts wohl nicht mehr wo das möglicherweise relevant wäre.
Die Bitreihenfolge von Integern ist an sich transparent, weil man in C
nur auf ganze Bytes zugreifen kann. Die sehen immer so aus, wie man es
erwartet (links msb, rechts lsb), selbst wenn da ein 4- oder
1-Bit-Prozessor hinterstecken würde. Ausnahme sind nur Bitfelder.
Nur garantiert einem niemand, dass Fließkommazahlen die gleiche
Bitreihenfolge wie Integer verwenden (zumindest afaik, bitte korrigieren
falls falsch). Sie könnten theoretisch also auch genau umgedreht sein,
wenn die FPU des Prozessors des so brauchen sollte. Oder auch in
Big-Endian statt Little-Endian im Speicher stehen, obwohl Integer auf
dem Prozessor Little-Endian sind. In der Praxis wohl unwahrscheinlich,
aber es wäre gemäß C-Standard.
Denn einen float hast Du legal nur in Rechenoperationen zu verwenden. Du
kannst ihn mit einem Cast zurück in einen Integer konvertieren (also den
ganzzahligen Wert bekommen), dann kümmert sich der Compiler um die
nötigen Schritte, also die entsprechenden Maschinenbefehle auszuführen,
dass Du das erwartetete Ergebnis bekommst. Aber wie er das im Speicher
vollbringt, ist seine Sache. Auch mit memcpy bekommst Du nur Bits, die
zu dem float gehören, aber keine garantierte Semantik dazu.
Es sei denn, der IEEE-Standard würde explizit vorschreiben, dass bei
Interpretation der Bits als Integer gemäß der Integer-Bytereihenfolge
der Plattform die richtige Float-Bitrepräsentation rauskommen muss. Dann
könnte man sich bei einem IEEE-Standard-konformen Prozessor wirklich
drauf verlassen. Ich weiß aber nicht, ob das da so drinnen steht.
Fabian O. schrieb:> Nur garantiert einem niemand, dass Fließkommazahlen die gleiche> Bitreihenfolge wie Integer verwenden (zumindest afaik, bitte korrigieren> falls falsch). Sie könnten theoretisch also auch genau umgedreht sein,> wenn die FPU des Prozessors des so brauchen sollte.
Wenn man so wie ich eher von der Hardware kommt, ist diese Annahme, dass
das tatsächlich in Silizium ausgeführt ist, ziemlich idiotisch. Jegliche
Konvertierngen zwischen float und int wird nur zusätzlich erschweren.
Die 23 bit Mantisse sind eben ein 24-bit int (inkl. implizite führende
1) und das nicht an das 32-bit int format anzupassen bzw. zumindestens
in den Bytes die bitreihenfolge gleich zu lassen, kann ich mir nicht
vorstellen.
Da jetzt kompatibilitätsproblem heraufzubeschwören halte ich für
übertrieben.
Rein theoretisch mag das ja stimmen.
Von Titel her erwarte ich mir wie man es wirklich plattformunabhängig
machen kann. Die Empfehlung:
Wenn Fließkommazahlen über einen Datenbus bzw. Netzwerk übertragen
werden sollen, empfiehlt sich die Wandlung in Festkommazahlen oder
Strings.
ist halt nur sehr allgemein gehalten und geht nicht auf auch da mögliche
Proble ein:
Manche Dezimalzahlen lassen sich im float format nicht exakt darstellen,
daher kann es bei Umwandlung in Strings zu Rundungsproblemen kommen. NAN
und INFINTY ... müssen auch richtig übertragen werden.
Also wie mache ich es plattformübergreifend?
Fritz schrieb:> Manche Dezimalzahlen lassen sich im float format nicht exakt darstellen,> daher kann es bei Umwandlung in Strings zu Rundungsproblemen kommen.
Dieses Umwandlungsproblem entsteht doch bei der Umwandlung von Dezimal
nach Float, nicht auf dem Rückweg. Wenn das ein Problem ist, darfst Du
erst gar keinen Float verwenden. In einem String kannst Du jede
beliebige Float-Zahl (und auch jede Dezimalzahl) darstellen.
> NAN und INFINTY ... müssen auch richtig übertragen werden.
Müssen sie das wirklich? Dann übertrag "NAN" und "INFINITY" als String.
Oder denk Dir ein anderes Protokoll aus. Alternativ kannst Du Exponent
und Mantisse getrennt als Integer übertragen. Johann hat ja oben die
entsprechenden Funktionen genannt, mit denen man daran kommt. Das geht
ohne Genauigkeitsverlust.
In der Praxis fällt mir aber ehrlich gesagt keine Anwendung ein, bei der
man Fließkomma mit maximaler Genauigkeit zwischen eingebetteten Systemen
übertragen muss. In der Regel hat man doch relativ klare Wertebereiche,
in denen sich z.B. Messwerte abspielen. Die skaliert man auf einen
passend großen Integer und gut ists.
Oder Du lebst eben damit, dass der Code für Deine Spezialanwendung nicht
portabel ist. Aber wie gesagt, woher kommen denn die Floats? Die tauchen
ja nicht auf natürliche Weise auf, sondern entstehen erst durch
Berechnungen. Woher kommen die Eingabewerte? Sind die so exakt, dass man
auf kein Mantissenbit verzichten kann?
Fritz schrieb:> Wenn man so wie ich eher von der Hardware kommt, ist diese Annahme, dass> das tatsächlich in Silizium ausgeführt ist, ziemlich idiotisch.
"Although the ubiquitous x86 of today use little-endian storage for all
types of data (integer, floating point, BCD), there have been a few
historical machines where floating point numbers were represented in
big-endian form while integers were represented in little-endian
form.[3] There are old ARM processors that have half little-endian, half
big-endian floating point representation."
http://en.wikipedia.org/wiki/Endianness#Floating-point_and_endianness> Also wie mache ich es plattformübergreifend?
Als ASCII-Darstellung mit einer Zweierpotenz statt 10 als Basis hat man
Umrechnungseffekte von und zum üblichen Format vom Hals. Aber wenn C -
was ich annehme - keine binäre Fliesskommadarstellung definiert, dann
geht das theoretisch überhaupt nicht völlig verlustfrei, weil dann auch
eine interne dezimale Fliesskommadarstellung und -rechnung zulässig wäre
- sowas gibts in Hardware wirklich, auch heutzutage!
Danke für den Link, hab ihn in den Artikel mit aufgenommen. Also ist es
schon so, wie ich angenommen habe: Auch der IEEE-Standard sagt nicht,
wie die Bytes intern zu speichern sind. Der C-Standard sowieso nicht,
der spezifiziert wirklich nur dezimale Mindest-Wertebereiche.
Erstaunlich, was die Leute alles portabel haben wollen. Dabei sind die
Anwendungen Welten voneinander weg. Lasst mich mal aufzaehlen, was man
auf'm PC sicher nicht braucht, auf einem Controller aber eben schon.
- interrupts, Echtzeit konzepte, Ringbuffer, FIR & IIR filter, PID,
Special function register, analog & digital IO, Tastengeschichten,
Bootloader, ...
Dinge, die man aufm PC hat, aber auf einem Controller nicht braucht.
- Dynamisches Memory, Klassen- & Klassen bibliotheken, Betriebssystem
der Ueberlapp zwischen Controller und PC ist IMO relativ bescheiden. Zu
bescheiden , um von Portabilitaet zu reden, resp zu verlangen.
Es geht ja nicht nur um Portierbarkeit PC <-> µC-Hänfling sondern auch
Portierbarkeit zwischen Boliden oder unterschiedlichen µCs.
Und wenn man sich High-End µCs anschaut, wo bereits fett ausgestattete
Multicore-Maschinen (SIMD, Vektor-Instruktionen, HW-float, ...) den
Alltag erreicht haben (Automotive), ist es zu den PC-Boliden nicht mehr
sooo weit.
Ausserdem geht es in dem Artikel darum, mochglichst viele Facetten der
Portierbarkeit von C Programmen zu beleuchten, hab ich zumindest so
verstanden.
Der ganze Artikel wäre doch für die Katz, wenn bestimmte Aspekte
willkürlich übergangen würden.
Fabian O. schrieb:>> NAN und INFINTY ... müssen auch richtig übertragen werden.>> Müssen sie das wirklich? Dann übertrag "NAN" und "INFINITY" als String.> Oder denk Dir ein anderes Protokoll aus.
Wenn ich das mache, habe ich ein proprietäres Format das ich auf beiden
Plattformen extra implementieren muß!
Da mache ich es lieber wie von mir vorher beschrieben über
Pointercasting und implementiere ein byteumordnung falls wirklich
notwendig.
Das funktioniert entweder auf Anhieb ohne Änderung oder das Problem
sticht sofort ins Auge (in einem float vertauschte bits, bytes) und
fallen so stark auf, die Byteumordnung ist dann aber nur mehr ein
Klacks.
Plattformunabhängig ist weder das eine noch das andere.
Ich verstehe nicht warum man ein wirklich schön definiertes Format
(IEEE) durch Zerstören (Stringumwandlung) und wieder Rekonstruieren
besser behandelt, als wenn man sich die Unterschiede der Plattform
anschaut und entsprechend umsetzt.
Zu meinen bisherigen Erfahrungen dazu. Mein Spezialgebiet ist die
Strom-, Spannungs-, Leistungsmessung im Einphasen- und Drehstromnetzen.
Habe auf folgenden Plattformen mit floats gearbeitet:
68000 Kommunikation mit PC
Embedded Arm mit uLinux + TI 6713 floatingpoint DSP und Anbindung an PC
Hat aber dabei keine Plattformprobleme mit den floats gegeben, auch ohne
UM- und Rückwandlung in von Strings.
Fabian O. schrieb:> In der Regel hat man doch relativ klare Wertebereiche,> in denen sich z.B. Messwerte abspielen. Die skaliert man auf einen> passend großen Integer und gut ists.
Na ja?
Ich muss z.B. beim Strom mA bis KA behandeln, da entsprechende
verschiedene Stromwandler/Shunts davor sitzen.
Bei der Spannung gehts auch von V bis MV (für EVUs).
Da mit einem float zu arbeiten ist wesentlich konfortabler als 2 Integer
für Meßwert und Skalierungsfaktor. Habe beides in der Praxis gemacht und
weiss wovon ich spreche.
Zu der von dir angesprochenen Auflösung ob 24-bit wirklich notwendig
sind.
Wenn man einiges an Mathematik (Winkelfuntionen, Wurzel ..) auch noch
braucht muß man doch auch bei 32-bit int verdammt aufpassen nichts an
Genauigkeit zu verlieren.
Fritz schrieb:> Ich verstehe nicht warum man ein wirklich schön definiertes Format> (IEEE) durch Zerstören (Stringumwandlung) und wieder Rekonstruieren> besser behandelt, als wenn man sich die Unterschiede der Plattform> anschaut und entsprechend umsetzt.
Der Artikel geht eben genau darum, was man beachten muss, dass der
gleiche C-Code auf verschiedenen Plattformen ohne Änderung läuft.
Nicht, wie man am effizientsten Fließkommazahlen überträgt. Beides
gleichzeitig geht eben leider nicht. Wenn es Dir nichts ausmacht, den
Code für einen anderen Prozessor ggf. ändern zu müssen, hält Dich ja
niemand davon ab, es so zu machen. Außer, dass Du wie gesagt besser
memcpy() statt dem Pointercast nehmen solltest.
Fritz schrieb:> Da mit einem float zu arbeiten ist wesentlich konfortabler als 2 Integer> für Meßwert und Skalierungsfaktor. Habe beides in der Praxis gemacht und> weiss wovon ich spreche.
Dass das komfortabler ist, hat niemand bestritten. Die meisten der
nicht-portablen Beispiele sind komfortabler zu schreiben. Unkomfortabel
wirds erst, wenn der Code nach dem Portieren plötzlich nicht mehr wie
erwartet funktioniert, und man vor allem zunächst nicht weiß, warum.
Fritz schrieb:> Zu der von dir angesprochenen Auflösung ob 24-bit wirklich notwendig> sind.> Wenn man einiges an Mathematik (Winkelfuntionen, Wurzel ..) auch noch> braucht muß man doch auch bei 32-bit int verdammt aufpassen nichts an> Genauigkeit zu verlieren.
Für Berechnungen (Zwischenergebnisse) keine Frage. Aber ist es nicht
eher so, dass entweder der Mikrocontroller mit der Messeinrichtung alle
Berechnungen durchführt und dann die Endergebnisse zurückgibt? Oder eben
nur die Rohwerte misst und die Berechnung z.B. an den PC delegiert.
Zwischenergebnisse, die so hohe Präzision erfordern, über die Leitung zu
schicken, halte ich konzeptionell für fragwürdig.
Aber mag sein, dass es auch sowas gibt. Dann muss man eben entweder den
Aufwand treiben, das sauber zu serialisieren oder man verzichtet auf
plattformunabhängigen Code.
Fabian O. schrieb:> Der Artikel geht eben genau darum, was man beachten muss, dass der> gleiche C-Code auf verschiedenen Plattformen ohne Änderung läuft.> Nicht, wie man am effizientsten Fließkommazahlen überträgt.
Du hast in deiner Gleitkommaabsatz von Übertragung zu anderen Platformen
gesprochen mit extra zu implementierenden Stringformatierungen. Die von
mir vorgeschlagene Pointercastoperation ist aber nur notwendig wenn man
extern kommuniziert und nicht intern im C-code.
Nach unserer langen Diskussion bin ich zu der Erkenntnis gekommen, dass
die Verwendung von float (IEEE definitionsgemäß) viel besser geeignet
ist als int um portablen Code zu schreiben. Also sollte man eher float
verwenden als int wenn es die Performance zuläßt.
Fabian O. schrieb:> Fritz schrieb:>> Da mit einem float zu arbeiten ist wesentlich konfortabler als 2 Integer>> für Meßwert und Skalierungsfaktor. Habe beides in der Praxis gemacht und>> weiss wovon ich spreche.>> Dass das komfortabler ist, hat niemand bestritten. Die meisten der> nicht-portablen Beispiele sind komfortabler zu schreiben. Unkomfortabel> wirds erst, wenn der Code nach dem Portieren plötzlich nicht mehr wie> erwartet funktioniert, und man vor allem zunächst nicht weiß, warum.
1.) Pointercasting brauche ich im C-Programm ohne Kommunikation nicht.
2.) Alle C-Compiler die vorgeben IEEE-floats zu verwenden egal ob per SW
oder FPU werden keine Probleme bei der Portierung machen, wenn der
Compiler nicht fehlerhaft ist.
3.) Die Verwendung von floats erlaubt eine besseres
Abstrahierungsvermögen als integer. Eine Trennung von Hardware zu
Applikation ist klarer möglich.
Dazu folgendes Beispiel aus meiner Praxis die Messung einer Spannung:
Am Anfang steht der ADC mit seinen unterschiedlichen Auflösungen 8, 10,
12 16 bit oder was auch immer. Diesn Wert mußt du in ein Format bringen,
das am besten zur Weiterverarbeitung geeignet ist. Meist dann ein
Fixpointformat um z.B. eine FFT zu berechnen. Dazu muß ich einen
Skalierungsfaktor berechnen der auch die Kalibrierung mit einschließt.
Nach der FFT-Berechnung kann ich dann mittels diesen Skalierungsfaktor
auf den physikalischen Meßwert kommen. Für verschiedene physikalische
Messwerte wie Spannung, Strom, Leistung, .. brauche ich jeweils eigene
Skalierungsfaktoren, die möglicherweise auch noch miteinander verknüpft
sind. Das läßt sich alles sauber mit int programmieren, keine Frage.
Aber: wenn ich gleich nach der AD-Wandlung in einer routine den AD-Wert
in ein float der entsprechenden physikalischen Größe umwandlen. Weitere
Berechnungen entsprechen direkt den mathematischen Formeln (Z.B. FFT)
und müssen nachher nicht mehr umskaliert werden.
Was ich damit sagen will ist, dass die Abstrahierung von ADC-Wert in
einem Schritt zum physikalischem Meßwert als float sinnvoll und leicht
verständlich ist. Die unportablen Teile der Anwendung können so klar in
die ADC->float routine gekapselt werden, der Rest ist dann völlig
portabel. Das habe ich mit "komfortabel" gemeint.
Fabian O. schrieb:> Der Compiler darf Dir> solche Konstrukte in C99 ggf. wegoptimieren. Wirklich legal ist es nur> per memcpy().
float fl;
int32 data32;
fl = *((float*)&data32);
Was soll denn bei der Optimierung anderes herauskommen als ein memcpy?
Fritz schrieb:> Also sollte man eher float> verwenden als int wenn es die Performance zuläßt.
Soso. Und warum meidet man float auf Mikrocontrollern wie die Pest?
Fritz schrieb:> fl = *((float*)&data32);>> Was soll denn bei der Optimierung anderes herauskommen als ein memcpy?
Wenn man per float * auf Daten zugreift, die eigentlich uint32_t sind,
dann sieht der Compiler keinen möglichen Konflikt dazwischen und es kann
sein, dass die aktuelle Version der Daten nur im Register und nicht im
Speicher steht. Haben Pointer und Daten den gleichen Typ (oder
irgendeinen char*), dann weiss er, dass es einen Konflikt geben kann.
Stell dir vor, der Compiler trennt dieses Statement in
float *p = (float*)&data32;
float f = *p;
und verzögert das zweite davon an eine Stelle, an der der aktuelle Wert
von data32 im Register steht und nicht im Speicher. Ich weiss aber
nicht, ob das bei einem solchen Beispiel realistisch vorkommen wird,
oder ob Compiler angesichts von &data32 ohnehin auf "Vorsicht" schalten.
Performer schrieb:> Soso. Und warum meidet man float auf Mikrocontrollern wie die Pest?
War wohl eher der Zwang float nicht zu verwenden.
1.) Weil viele uCs keine FPU haben,
2.) Weil die libs für float sehr viel Platz brauchen, speziell wenn man
auch printf verwendet, und die kleinen uCs eben nicht genung flash
haben.
3.) Weil die Performance von SW float sehr schlecht ist.
4.) Weil ein float 4 byte braucht anstatt 2 für ein int16 (also mehr RAM
braucht).
5.) Weil die freien abgespeckten Versionen der kommerziellen IDEs nur
32K Programm erlauben und damit die Verwendung der flaot-libs praktisch
unmöglich machen.
Das hat sich aber mit der Einführung des ARM Cortex M4 mit FPU doch
stark geändert.
Jetzt spielen nur noch die 4. u. 5. eine Rolle.
Fritz schrieb:> Das hat sich aber mit der Einführung des ARM Cortex M4 mit FPU doch> stark geändert.> Jetzt spielen nur noch die 4. u. 5. eine Rolle.
Aso. Wenn plattformübergreifend > "ARM Cortex M4 mit FPU" bedeutet ...
Fritz schrieb:> Du hast in deiner Gleitkommaabsatz von Übertragung zu anderen Platformen> gesprochen mit extra zu implementierenden Stringformatierungen. Die von> mir vorgeschlagene Pointercastoperation ist aber nur notwendig wenn man> extern kommuniziert und nicht intern im C-code.>> [...]>> 1.) Pointercasting brauche ich im C-Programm ohne Kommunikation nicht.
Genau darum geht es doch die ganze Zeit: Wie man Fließkommazahlen nach
draußen kommuniziert, z.B. vom Mikrocontroller zum PC. Innerhalb des
Mikrocontrollers oder innerhalb des PCs kannst Du so viel mit float
rechnen, wie Du lustig bist. Das gibt überhaupt keine Probleme
hinsichtlich Portabilität.
Deshalb ist mir auch nicht so klar, was Du mit dem restlichen Text sagen
möchtest. Klar kannst Du das innerhalb des C-Programms alles mit float
rechnen. Dafür gibts den Datentyp ja. Das Problem entsteht erst, wenn Du
mit Type punning (Pointer Cast) am C-Typsystem vorbei auf die interne
Bitrepräsentation zugreifst. Da gibt es nämlich keine Garantien mehr,
dass die zwischen Mikrocontroller und PC gleich ist.
Fritz schrieb:> float fl;> int32 data32;>> fl = *((float*)&data32);>> Was soll denn bei der Optimierung anderes herauskommen als ein memcpy?
1
voidprocess(void){
2
floatfl;
3
int32data32=buffer[0];// <- darf wegoptimiert werden
4
5
fl=*((float*)&data32);
6
process_data(fl);
7
}
Der Compiler darf die Zuweisung an data32 wegoptimieren, da data32 in
der Funktion nicht mehr über seinen Namen, einen int32-Pointer oder
char-Pointer gelesen wird. Der float* darf gemäß der Aliasing-Regel
nicht auf einen int32 zeigen.
Performer schrieb:> Soso. Und warum meidet man float auf Mikrocontrollern wie die Pest?
Das habe ich mich auch schon gelegentlich gefragt. Klar, bei 4KB Tinys
ist das Unfug. Aber schon bei 8-Bittern mit 32K Flash kann float in der
Programmierung wesentlich angenehmer sein als eine Umgehung durch
Festkommaarithmetik, und vom Platzverbrauch evtl. tolerierbar.
Insbesondere es nur um Berechnungen geht und platzraubende Ein/Ausgabe
nicht erforderlich ist. Nicht in jedem Fall ist die Performance wirklich
kritisch.
A. K. schrieb:> Stell dir vor, der Compiler trennt dieses Statement in> float *p = (float*)&data32;> float f = *p;> und verzögert das zweite davon an eine Stelle, an der der aktuelle Wert> von data32 im Register steht und nicht im Speicher. Ich weiss aber> nicht, ob das bei einem solchen Beispiel realistisch vorkommen wird,> oder ob Compiler angesichts von &data32 ohnehin auf "Vorsicht" schalten.
Sorry, das kann ich mir nicht vorstellen.
Mein C++Builder macht das optimiert oder nicht optimiert so wie memcpy.
Was sagen denn die C-Standard Spezialisten dazu?
fl = *((float*)&data32);
Sollte das nicht im Standard eine eindeutige Definition haben?
Fritz schrieb:> Sollte das nicht im Standard eine eindeutige Definition haben?
§ 6.5 Abs. 7 im C99-Standard:
http://www.open-std.org/jtc1/sc22/WG14/www/docs/n1256.pdf
An object shall have its stored value accessed only by an lvalue
expression that has one of the following types:
— a type compatible with the effective type of the object,
— a qualified version of a type compatible with the effective type of
the object,
— a type that is the signed or unsigned type corresponding to the
effective type of the object,
— a type that is the signed or unsigned type corresponding to a
qualified version of the effective type of the object,
— an aggregate or union type that includes one of the aforementioned
types among its members (including, recursively, a member of a
subaggregate or contained union), or
— a character type.
Ist übrigens alles im Artikel verlinkt ...
Vorab möchte ich feststellen, dass Memcpy sicher die bessere Lösung ist.
Aber um das eindeutig zu klären bitte
Fabian O. schrieb:> Der Compiler darf die Zuweisung an data32 wegoptimieren, da data32 in> der Funktion nicht mehr über seinen Namen, einen int32-Pointer oder> char-Pointer gelesen wird. Der float* darf gemäß der Aliasing-Regel> nicht auf einen int32 zeigen.
Was ist jetzt genau die Aliasing-Regel, wenn das ein Fehler ist, warum
gibt der Compiler keine Fehlermeldung aus?
Warum kann er wegoptimieren ist &data32 kein int32-Pointer?
>Was sagen denn die C-Standard Spezialisten dazu?>fl = *((float*)&data32);
Nur zum Verständnis: Was ist an diesem Verfahren besser, als eine Union
zu verwenden?
franz schrieb:> Nur zum Verständnis: Was ist an
diesem Verfahren besser, als eine Union
> zu verwenden?
Bei der Union darf man offiziell nur jene Komponente lesen, die man
vorher reingeschrieben hat. Also in
union {
uint32_t u32;
float f32;
};
nicht u32 schreiben und danach f32 lesen.
Fritz schrieb:> Mein C++Builder macht das optimiert oder nicht optimiert so wie memcpy.
Reale Compiler sind hier irrelevant.
> Sorry, das kann ich mir nicht vorstellen.
Weshalb nicht? Casts sind eine recht gefährliche Ecke und hebeln einige
Sicherheitsmechanismen aus. Ich empfehle daher, sie sparsam einzusetzen.
franz schrieb:>>Was sagen denn die C-Standard Spezialisten dazu?>>fl = *((float*)&data32);>> Nur zum Verständnis: Was ist an diesem Verfahren besser, als eine Union> zu verwenden?
Nichts, beides ist unspecified (type punning).
Fritz schrieb:> Was ist jetzt genau die Aliasing-Regel, wenn das ein Fehler ist, warum> gibt der Compiler keine Fehlermeldung aus?
Also GCC kann das, etwa mit -fsyntax-only -Werror=strict-aliasing.
1
#include<stdint.h>
2
3
floatfun(uint32_ti)
4
{
5
return*(float*)&i;
6
}
1
<stdin>: In function 'fun':
2
<stdin>:5:5: error: dereferencing type-punned pointer will break strict-aliasing rules [-Werror=strict-aliasing]
3
cc1.exe: some warnings being treated as errors
Übrigens optimiert GCC mit Optimierung bei der memcpy-Version
1
#include<string.h>
2
#include<stdint.h>
3
4
floatfun(uint32_ti)
5
{
6
floatf;
7
memcpy(&f,&i,sizeof(f));
8
returnf;
9
}
den memcpy-Ausruf komplett raus. Getestet mit GCC 4.3, 4.5, 4.6, 4.7
und 4.8.
In meinem Umfeld wäre der float-Cast übrigens kein Problem, solcher Code
würde schon durch kein Review kommen ;-)
>Bei der Union darf man offiziell nur jene Komponente lesen, die man>vorher reingeschrieben hat. Also in> union {> uint32_t u32;> float f32;> };>nicht u32 schreiben und danach f32 lesen.
Danke.
BTW: Ich habe einen Vorschlag zum Theman Datenübertragung eines Floats
und korrekter Interpretation. Man könnte die Mantisse und den Exponent
berechnen und dann die beiden übertragen. Ich weiß, es ist umständlich,
aber Fehlinterpretationen sind dann ausgeschlossen.
Weil "mein Compiler hat damit kein Problem" reinweg nichts über
realistische bis theoretische Portabilitätskriterien und entsprechende
Problemfelder aussagt.
franz schrieb:> BTW: Ich habe einen Vorschlag zum Theman Datenübertragung eines Floats> und korrekter Interpretation. Man könnte die Mantisse und den Exponent> berechnen und dann die beiden übertragen. Ich weiß, es ist umständlich,> aber Fehlinterpretationen sind dann ausgeschlossen.
Halte ich auch für eine gute Lösung.
Leider geben uns ja hier die Spezialisten nur Antworten wie man es
falsch oder nicht C-spezifikationsgerecht macht, aber nicht wie man das
IEEE-Bitmuster korrekt von einer auf die andere Plattform überträgt.
Ich persönlich würde am liebsten 4 einzelne Bytes in einer definierten
Reihenfolge (z.B. Mant0-Mant7, Mant8-Mant15, Mant16-Mant23-E0, E1-E7-V)
übertragen.
Das könnte sehr wohl in jeweils einer Lese und Schreibroutine in C für
meinen Compiler gekapselt und geschreiben werden. Wie weit das aber
plattformübergreifend C-spzifisch OK ist kann ich nicht sagen. Haben wir
ja oben diskutiert.
Hilfreich in dem Artikel wäre auch wenn man im Compiler per möglicher
globale defines spezifisch auf big- bzw. littleendian oder
integerbitlänge reagieren kann.
Gibt es dafür Konstanten?
A. K. (prx) schrieb:
> Weil "mein Compiler hat damit kein Problem" reinweg nichts über> realistische bis theoretische Portabilitätskriterien und entsprechende> Problemfelder aussagt.
Interessant. Dann ist das ganze hier wohl nur ein Trockenkurs für
Theoretiker. Ich dachte die Artikel sollten von praktischer Relevanz
sein, wegen
"Andreas Schwarz schrieb:
Das Hauptkriterium ist: wie interessant und nützlich ist der Artikel
für den Leser;"
Naja, Theorie kann ja auch ganz interessant sein. Den Nerd wird's
freuen. Der Beamtenapparat ergötzt sich schließlich auch am
Kleingedruckten seiner Verordnungen.
:-)
A. K. schrieb:> Weil "mein Compiler hat damit kein Problem" reinweg nichts über> realistische bis theoretische Portabilitätskriterien und entsprechende> Problemfelder aussagt.
Theoretisch OK!
Realistisch gibt es zumindesten Lösungen zwischen realen Plattformen die
offensichtlich keine Portabilitäsprobleme generiren.
Aber ich warte noch immer auf eine theoretisch korrekte Lösung?
Fritz schrieb:> Leider geben uns ja hier die Spezialisten nur Antworten wie man es> falsch oder nicht C-spezifikationsgerecht macht, aber nicht wie man das> IEEE-Bitmuster korrekt von einer auf die andere Plattform überträgt.
Es gibt doch verschiedene Lösungsansätze. Nur gefallen sie Dir offenbar
alle nicht.
- Umwandeln in String. Fehlinterpretation ausgeschlossen, sogar
menschenlesbar.
- Umwandeln in Festkomma. Sehr effizient, sehr einfach zu definieren.
- Getrennte Übertragung von Exponent und Mantisse als Integer.
Letzteres geht mit den Funktionen aus der Standardbibliothek in einer
Zeile:
1
#include<inttypes.h>
2
#include<math.h>
3
#include<stdio.h>
4
5
serialize(doubleval,int*exp,int32_t*sgn)
6
{
7
*sgn=frexp(val,exp)*pow(2,31);
8
}
9
10
doubledeserialize(intexp,int32_tsgn)
11
{
12
returnldexp((double)sgn/pow(2,31),exp);
13
}
14
15
voidtest(doubleoriginal)
16
{
17
doublerebuild;
18
intexp;
19
int32_tsgn;
20
21
serialize(original,&exp,&sgn);
22
rebuild=deserialize(exp,sgn);
23
24
printf("orginal: %f\n",original);
25
printf("rebuild: %f\n",rebuild);
26
printf("exp: %i\n",exp);
27
printf("sgn: %"PRIX32"\n",sgn);
28
}
29
30
intmain(void)
31
{
32
floatoriginal_float=-98765.4321;
33
doubleoriginal_double=-98765.4321;
34
35
printf("test float\n");
36
test(original_float);
37
38
printf("\ntest double\n");
39
test(original_double);
40
}
Ausgabe:
1
test float
2
orginal: -98765.429688
3
rebuild: -98765.429688
4
exp: 17
5
sgn: 9F8CA480
6
7
test double
8
orginal: -98765.432100
9
rebuild: -98765.432068
10
exp: 17
11
sgn: 9F8CA459
Die Mantisse wird also als 32-Bit-Integer übertragen, als Exponent
würden 8 Bit reichen. Serialisieren / Deserialisieren geht in einer
Zeile mit Standardfunktionen. Die Genauigkeit ist sogar höher als ein
einfacher float.
Wenn Dir das alles nicht gefällt, machst Du eben weiter Type Punning.
Oder Du friemelst die Bits von Hand ins IEEE-Format, geht sicher auch.
Sehe nur keinen wirklichen Nutzen drinnen.
Fritz schrieb:> Hilfreich in dem Artikel wäre auch wenn man im Compiler per möglicher> globale defines spezifisch auf big- bzw. littleendian oder> integerbitlänge reagieren kann.>> Gibt es dafür Konstanten?
Ja, stehen im Artikel.
Adler schrieb:> Interessant. Dann ist das ganze hier wohl nur ein Trockenkurs für> Theoretiker. Ich dachte die Artikel sollten von praktischer Relevanz> sein, wegen
Portabilität bezieht sich nicht auf die Portierung von C++Builder V1.2.3
auf C++Builder V1.2.3. Daher nützt es reinweg nichts, wenn dieser
C++Builder V1.2.3 eine bestimmte Formulierung problemlos frisst, aber
andere Compiler damit Probleme bekommen könnten.
Ein Fokus des Artikels und des Threads liegt m.E. auf der Vermeidung von
Konstruktionen, die bestimmte Eigenschaften von Compilern voraussetzen,
die in der Sprache undefiniert oder unspezifiziert sind. Das hat
notwendigerweise viel mit der Sprachdefinition zu tun, weniger mit den
Eigenschaften von einem oder mehreren realen Compilern.
Der Thread ist allerdings mittlerweile ein wenig vom Thema Portabilität
von Programmen abgewandert und fokussiert sich nun eher auf Portabilität
von Datenformaten zwischen verschiedenen Systemen. Ist auch ein
interessantes Thema, aber ein anderes.
Bei Portabilität von Programmen wie Daten sind beide Aspekte relevant.
Sowohl die theoretische sich aus der Sprachdefinition ergebende
Situation als auch die praktische sich aus realen Maschinen und
Compilern ergebene.
So erlaubt die Sprachdefinition Fliesskommaformate, die nicht auf IEEE
754 basieren, und das ist keine Theorie, denn solche Systeme existieren
nach wie vor in wirtschaftlich relevantem Umfang. Allerdings wird man
sich für praktische Betrachtungen derzeit eigentlich immer auf IEEE 754
beschränken können.
Ein weiterer ähnlicher Aspekt wird im Artikel kurz angeschnitten: die
Darstellbarkeit von -2^(N-1). Theoretisch muss man - dem Standard
folgend - darauf verzichten. Praktisch kann man wohl davon ausgehen,
dass für absehbare Zeit bei Ganzzahlen nur Zweierkomplementdarstellung
existieren wird.
Dass man mit allzu enger Fixierung auf praktische Betrachtungen aber auf
die Nase fallen kann zeigte die Jahr 2000 Umstellung. Viele Programme
aus früheren Jahrzehnten waren nie für den Einsatz in derart langem
Zeitraum gedacht und bekamen daher Probleme.
Das gleiche kann mit Programmen geschehen, die sich allzu sehr auf
bestimmte praktische Eigenschaften von Maschinen und Compilern fixieren.
Denn der Umstand, dass heutige reale Compiler und Maschinen bestimmte
Eigenschaften haben, garantiert nicht, dass es in 20 Jahren immer noch
so sein wird.
Ich hatte oben ein Beispiel gebracht, in dem der Optimizer ein C
Statement aufsplittet und einen Teil davon an eine andere Stelle
schiebt. C Compiler von anno 1980 setzten Statements ziemlich 1:1 um, da
konnte das nie geschehen, weshalb man damals beispielsweise auch kein
"volatile" brauchte. Heute sind solche Aktionen bei hochoptimierenden
Compilern Alltag. Manche Probleme mit GCC rühren daher, dass die
Optimierung dieses Compilers aus dem Highend-Sektor stammt und er daher
aggressiver und mit anderem Ziel vorgeht, als Compiler, die nur den
Mikrocontroller-Markt adressieren.
Fritz schrieb:> Aber ich warte noch immer auf eine theoretisch korrekte Lösung?
Korrekter als ASCII mit Einschränkung auf die definierten Wertebereiche
wirds nicht. Da Fliesskommadarstellung ohnehin nicht auf absolut exakte
Ergebnisse abzielt muss das ausreichen. Wers noch weiter treiben will
nimmt ASCII Darstellung auf Hex-Basis, was im Rahmen binärer
Internformate exakt umsetzbar ist.
Eine theoretisch in jedem Fall absolut korrekte Lösung scheitert m.E.
an der Offenheit der Spezifikation der Sprache.
Fritz schrieb:> Leider geben uns ja hier die Spezialisten nur Antworten wie man es> falsch oder nicht C-spezifikationsgerecht macht, aber nicht wie man das> IEEE-Bitmuster korrekt von einer auf die andere Plattform überträgt.Beitrag "Re: Feedback zum Artikel "Plattformunabhängige Programmierung in C""> Hilfreich in dem Artikel wäre auch wenn man im Compiler per möglicher> globale defines spezifisch auf big- bzw. littleendian oder> integerbitlänge reagieren kann.>> Gibt es dafür Konstanten?
AFAIK nicht plattformübergreifend. Bei GCC gibt's immerhin Makros wie in
Beitrag "Re: Feedback zum Artikel "Plattformunabhängige Programmierung in C""
Ansonsten muss man sich seine eigene hostconfig.h erzeugen, etwa
Sowas hab ich bisher erst einmal gebraucht um Endianess aus einem
Programm zu faktorisieren. Ein Test zur Laufzeit war zu aufwändig und
je nach Compiler nicht optimiert.
Obiger Ansatz setzt allerdings voraus, daß das Programm auf dem
jeweiligen Host ausgeführt werden kann, was nicht ganz trivial ist, etwa
wenn das Programm auf einem µC zu hosten ist.
Teilweise lassen sich Host-Features auch zur Compilezeit, d.h. auf der
Build-Plattform erfragen, so dass kein Code auf dem Host ausgeführt
werden muss. Etwa:
1
inta[sizeof(int)==2?1:-1];
Was je nach Host zu einem Compile-Fehler führt (int != 2 Byte) oder
durchgeht (int = 2 Byte).
Damit ist man dann bei den Auto-Tools (autoconf, etc.) angelangt...
Fabian O. schrieb:> Wenn Dir das alles nicht gefällt, machst Du eben weiter Type Punning.> Oder Du friemelst die Bits von Hand ins IEEE-Format, geht sicher auch.> Sehe nur keinen wirklichen Nutzen drinnen.
Irgendwie scheine ich dir auf die Nerven zu gehen?
Aber schlußendlich hast du nun eine Lösung (serialize, deserialize von
oben) präsentiert, die C-konform ist und bezüglich Performance und
benötigten Bytes für die Kommunikation einer Stringum- und Rückwandlung
überlegen ist. Auch Infinity wird (meinem kurzem Test zufolge) richtig
übertragen.
Wenn ich deine im Artikel erwähnte Formulierung
" empfiehlt sich die Wandlung in Festkommazahlen oder Strings. "
ansehe, kommt auch ein geübter C-programmierer nicht sofort auf obige
Lösung. Da du aber die Integerproblematik sehr ausführlich behandelt und
mit Beispielen versehen hast, würde ich es sehr begrüßen, wenn du obige
Funktionen Serialize und Deserialize als korrekte Umwandlung einfügen
würdest.
Aber bitte für float und double getrennt jeweils auch mit den
entsprechend richtigen math.h Funtionen
für float: frexpf, ldexpf, powf
für double: frexp, ldexp, pow
Ob du auch noch Stringum- und Rückwandlung als Alernative drinnen lassen
möchtest, sei dir überlassen.
Fritz schrieb:> Irgendwie scheine ich dir auf die Nerven zu gehen?
Naja, es ist schon etwas mühsam, wenn Du Dich immer wieder darüber
beschwerst, dass in einem Artikel über plattformunabhängigen Code nicht
empfohlen wird, plattformabhängige Konstrukte zu verwenden ...
Wie gesagt, es zwingt Dich niemand, Deinen Code plattformunabhängig zu
machen. Das meine ich wirklich völlig neutral. Es ist ja (zumindest imo)
genau eine der Stärken von C, dass man solche extrem effizienten
Lösungen wie die interne Repräsentation eines floats als int zu
interpretieren, überhaupt hat. Damit kann man ja auch tatsächlich
nützliche Dinge machen:
http://en.wikipedia.org/wiki/Fast_inverse_square_root
In vielen anderen Hochsprachen hat man keine Chance, das hinzubekommen.
Ebenso ist toll, dass es C für nahezu jeden Prozessor gibt. Nur kann man
eben nicht auch noch erwarten, dass extrem effiziente Konstrukte, die
auf maschinenabhängigen Eigenschaften basieren, auch noch portabel sind.
Man kann nicht alles haben.
Der Artikel soll zeigen, auf was man aufpassen muss, wenn man seinen
Code auf verschiedenen Plattformen einsetzen will und was prinzipiell
schief gehen kann. Wenn Du sagst, mein Code wird nur auf Prozessoren mit
gleicher Float-Repräsentation laufen (und damit hast Du ja auch ziemlich
wahrscheinlich recht), dann bitte, mach es so. Ich benutze zum Beispiel
auch ohne schlechtes Gewissen uint8_t statt unsigned char oder
uint_least8_t, obwohl es uint8_t nicht auf jeder Plattform gibt. Einfach
weil ichs leserlicher finde und nicht erwarte, dass der Code mal auf
einem komischen DSP laufen muss ... Aber ich finde es sinnvoll, solche
Dinge zumindest zu wissen und im Hinterkopf haben.
> Aber schlußendlich hast du nun eine Lösung (serialize, deserialize von> oben) präsentiert, die C-konform ist und bezüglich Performance und> benötigten Bytes für die Kommunikation einer Stringum- und Rückwandlung> überlegen ist. Auch Infinity wird (meinem kurzem Test zufolge) richtig> übertragen.>> Wenn ich deine im Artikel erwähnte Formulierung> " empfiehlt sich die Wandlung in Festkommazahlen oder Strings. "> ansehe, kommt auch ein geübter C-programmierer nicht sofort auf obige> Lösung. Da du aber die Integerproblematik sehr ausführlich behandelt und> mit Beispielen versehen hast, würde ich es sehr begrüßen, wenn du obige> Funktionen Serialize und Deserialize als korrekte Umwandlung einfügen> würdest.
Habe ich schon gemacht. Ich hatte die Funktionen beim Schreiben des
Artikels zugegebenermaßen auch nicht im Kopf. Die hat erst Johann L. in
seinem Beitrag ins Spiel gebracht. Bin aber auch nach wie vor der
Meinung, dass man mit Festkomma oder Strings als Netzwerkformat die
meisten Anwendungsfälle sehr gut bzw. besser abdecken kann. Aber gut,
die Diskussion hatten wir schon ...
> Aber bitte für float und double getrennt jeweils auch mit den> entsprechend richtigen math.h Funtionen> für float: frexpf, ldexpf, powf> für double: frexp, ldexp, pow
Die Varianten für float sind keine Standardfunktionen, ist also
hinsichtlich Portabilität nicht zu empfehlen. Auf Prozessoren mit FPU
ist es eh egal, weil der mit double genau so gut rechnen kann. Aber ich
schreibe noch nen Satz dazu, dass es die gibt.
> Ob du auch noch Stringum- und Rückwandlung als Alernative drinnen lassen> möchtest, sei dir überlassen.
Das IST eine Alternative, und zwar nicht die schlechteste. Warum sollte
man die nicht mal nennen sollen? Was in der jeweiligen Anwendung am
sinnvollsten ist, muss schon jeder selber entscheiden. Je mehr
Alternativen man zur Auswahl hat, desto besser.
Fabian O. schrieb:> Die Varianten für float sind keine Standardfunktionen, ist also> hinsichtlich Portabilität nicht zu empfehlen.
Korrektur: In C99 sind sie Teil des Standards.
Fabian O. schrieb:> Auf Prozessoren mit FPU> ist es eh egal, weil der mit double genau so gut rechnen kann.
Nein leider nicht!
Da wir hier im Mikrokontroller Forum sind:
Es gibt seit ein paar Jahren den ARM Cortex M4 schon von mehreren
Herstellern und auch sehr preiswert und inzwischen hier im Forum sehr
beliebt.
Der hat aber eine FPU die nur float kann aber leider keine double.
Hallo Fabian ich bins, muß nochmal lästig sein.
Du hast beide Varianten (float und double) in den Artikel gestellt, aber
die double Variante ist nicht korrekt. Du verwendest für die Mantisse
ein int32, was aber für ein double viel zu klein ist. Die integers für
die double routinen müssen int64 sein!
Weiterer Aspekt der Portierbarkeit ist das Verhalten von NANs, d.h. ob
es eine stille NaN ist (non-signaling) oder nicht (signaling).
Wenn man die Plattform wachselt kann man sich dann wahlweise wundern, wo
die Exceptions herkommen, oder warum keine mehr kommen und die
Algorithmen stattdessen Käse liefern :-)
Fritz schrieb:> Der hat aber eine FPU die nur float kann aber leider keine double.
OK, wusste ich nicht. Dann lohnt es sich natürlich, die Float-Funktionen
zu nutzen.
Fritz schrieb:> Du hast beide Varianten (float und double) in den Artikel gestellt, aber> die double Variante ist nicht korrekt. Du verwendest für die Mantisse> ein int32, was aber für ein double viel zu klein ist. Die integers für> die double routinen müssen int64 sein!
Naja, was heißt "korrekt". Ist halt die Frage, welche Genauigkeit man
braucht. Denk mal, für viele Anwendungen reichen 32 Bit völlig. Man
könnte im Prinzip jede beliebige Anzahl an Mantissenbits übertragen,
auch 16, 24, 32, 40, 48, je nach benötigter Genauigkeit.
Glaub ich stell das wieder zurück auf eine Variante als Beispiel. Ist ja
auch nicht dafür gedacht, 1:1 kopiert zu werden, sondern einfach zu
zeigen, wie man die Funktionen benutzen kann.
Wäre ein ldexp[f] (x, 31) nicht besser als ein x * pow[f] (2, 31) ?
Sowohl was die Effizienz als auch Rundungsfehler angeht. Zwar wird man
Rundungsartefakte haben, aber die müssen ja nicht größer sein als nötig.
Und was spricht eigentlich gegen ein "x * (1ul << 31)"?
Ich finde die Variante mit pow(2, 31) am klarsten. Macht es denn einen
Unterschied? Hätte erwartet, dass das zur Compilezeit durch die
entsprechende Konstante ersetzt wird. Oder kann der Compiler das nicht?
Fabian O. schrieb:> Ich finde die Variante mit pow(2, 31) am klarsten. Macht es denn einen> Unterschied? Hätte erwartet, dass das zur Compilezeit durch die> entsprechende Konstante ersetzt wird.
Jo, stimmt. Sind ja beide Parameter bekannt.
GCC 4.8 faltet das sogar ohne Optimierung...
Fabian O. schrieb:> Naja, was heißt "korrekt". Ist halt die Frage, welche Genauigkeit man> braucht. Denk mal, für viele Anwendungen reichen 32 Bit völlig. Man> könnte im Prinzip jede beliebige Anzahl an Mantissenbits übertragen,> auch 16, 24, 32, 40, 48, je nach benötigter Genauigkeit.
Einerseits bist du sehr genau und pedantisch was die Umsetzung der
Standards betrifft. Dann machst du aber Vorschläge die darauf
hinauslaufen:
wo double draufsteht ist aber nicht double drin!!??
Johann L. schrieb:> Weiterer Aspekt der Portierbarkeit ist das Verhalten von NANs, d.h. ob> es eine stille NaN ist (non-signaling) oder nicht (signaling).>> Wenn man die Plattform wachselt kann man sich dann wahlweise wundern, wo> die Exceptions herkommen, oder warum keine mehr kommen und die> Algorithmen stattdessen Käse liefern :-)
Da fürchte ich, dass die beiden routinen serilize und deserialize auch
Probleme machen könnten. Aber seis drum, irgendwie habe ich das Gefühl
wie wenn ich von Wien nach London fliegen will, aber mir die
Fluggesellschaft vorschlägt, dann fliegen Sie von Wien nach Stockholm
und steigen dort in die Maschine nach London um. Komme zwar auch
dorthin, für mich aber mit manchen Nachteilen dauert länger, Umwelt! ...
Ist halt ein Umweg.
Dabei könnte man mit einer Erweiterung der math.h - lib das Problem so
einfach, elegant und effizient (benötigt nur 4/8 byte, ist schneller)
lösen:
2 Funktionen:
int32_t toieebitsf(float x);
float fromieebitsf(int32_t i);
Ins integer werden die bits so hineinkopiert, wie es der IEEE Standard
vorsieht und umgekehrt.
Dasselbe entsprechend auch für double.
Käme auf den meisten Plattformen auf ein memcpy heraus. Wäre als in32_t
portabel. Ich dachte bisher C ist diejenige Hochsprache, die den
schnellsten und effizientesten Code erzeugen kann. Wie man sieht, geht
aber nicht.
Offensichtlich haben die Standardisierungsgremien ein sicher häufig
auftretendes Problem nicht berücksichtigt.
Fritz schrieb:> Ich dachte bisher C ist diejenige Hochsprache, die den> schnellsten und effizientesten Code erzeugen kann.
Das ist nicht der Fall. Die in C recht häufige Möglichkeit von
zulässigem Aliasing kann ein ziemliches Problem für einen C Optimizer
darstellen.
A. K. schrieb:> Das Überlaufverhalten müsste noch definiert werden.
Wieso? Im IEEE-Standard ist doch alles sehr gut definiert. Ich
transportiere doch nur ein Bitmuster von der einen Plattform auf die
andere. Beide Plattformen sollten doch dieses Bitmuster gleich nach IEEE
behandeln. Falls nicht ist die IEEE-Implementation in einer Plattform
halt nicht richtig, das kann aber auch bei anderer Umsetzung zu
Problemen führen.
A. K. schrieb:> Das ist nicht der Fall. Die in C recht häufige Möglichkeit von> zulässigem Aliasing kann ein ziemliches Problem für einen C Optimizer> darstellen.
Ich meinte nur im Vergleich zu anderen Hochsparchen, nicht zu
handgeschriebenen Assemblercode.
Der Artikel geht davon aus, dass man Plattformunabhängigkeit (=
portierbar) braucht. Meine Erfahrung in der Praxis ist, dass man das in
nahezu allen Fällen nicht braucht.
Das bedeutet, dass sich der Aufwand (Aufwand == Kosten) den man in
Plattformunabhängigkeit steckt nicht amortisiert. Daher sollte man es
lassen. Im Normalfall sind die verwendeten Algorithmen nicht so
kompliziert, dass man sie für alle Ewigkeit in eine portierbare
Bibliothek stecken muss, damit sich der ursprüngliche
Implementierungsaufwand über mehrere Portierungen amortisiert.
Damit ist an bei einer Grundeinstellung des agilen Entwickelns: YAGNI -
You ain't gona need it. Mache nichts, von dem du nicht heute schon
weißt, dass du es wirklich in der Zukunft brauchst. Bei der üblichen
Portierungsrate von Embedded-Anwendungen, die nahe Null liegt, weiß man
heute ziemlich sicher, dass man Portierbarkeit nicht braucht. Daher
lohnt es nicht.
Wer mit agilen Methoden nichts am Hut hat, der kennt trotzdem vielleicht
KISS - Keep it simple stupid. Mach es einfach. Auch das greift hier.
Portierbarkeit wird mit zusätzlichen Abstraktionsebenen erkauft. Je mehr
Code, desto mehr Fehler kann man einbauen, desto aufwendiger wird die
Wartung des Codes.
Und schließlich, plattformunabhängiger Code kann bedeuten, dass man
gegen die Architektur eines Mikrocontrollers kämpft, nur damit der Code
plattformunabhängig ist. Damit stellt man eine Eigenschaft
(Plattformunabhängigkeit) über eine effiziente Lösung jetzt und hier.
Bei einer Wahrscheinlichkeit von nahe Null dass man die
Plattformunabhängigkeit braucht ein sehr schlechter Tausch.
Und noch einer schrieb:> Der Artikel....
Du sprichst mir mit deinen Worten aus der Seele. Ich als alter
uC-Anwender sehe das genauso.
Ich habe halt versucht, mit meinen dauernden Fragen die Theoretiker
selbst draufkommen zu lassen wo halt der Unterschied zwischen Theorie
und Praxis liegt.
Fritz schrieb:>> Das Überlaufverhalten müsste noch definiert werden.>> Wieso? Im IEEE-Standard ist doch alles sehr gut definiert.
Da schon. Aber nicht in umgekehrter Richtung.
Klar, wenn ein Artikel "Brötchen backen für Fortgeschrittene"
geschrieben wird, komme ich auch nicht umhin die Leute in die Richtung
zu stupsen, daß die Praxis gezeigt hat, daß zum Frühstück Müsli zu essen
deutlich effizienter ist.
Fritz schrieb:> Ich meinte nur im Vergleich zu anderen Hochsparchen, nicht zu> handgeschriebenen Assemblercode.
Ich auch. Aber da liegt beispielsweise FORTRAN vorne.
Und noch einer schrieb:> Meine Erfahrung in der Praxis ist, dass man das in> nahezu allen Fällen nicht braucht.
Meine Erfahrung wiederum ist, dass man das in vielen Fällen brauchen
kann. Sowohl bei Mikrocontrollern, als auch in anderem Umfeld.
Beispiele:
- Code für 1-Wire- oder SHT11-Sensoren lässt sich weitgehend portabel
gestalten, nur der unmittelbare Portzugriff muss angepasst werden.
- Netzwerkprotokolle alle Art, beispielsweise RS485 oder CAN lassen sich
ausgezeichnet wiederverwenden, wenn der darunter liegende
systemabhängige Layer gleich ist.
> Und schließlich, plattformunabhängiger Code kann bedeuten, dass man> gegen die Architektur eines Mikrocontrollers kämpft
Der Flieskommakram geht in diese Richtung. Der Rest hingegen ist eher
eine Frage von "gewusst wie".
Fritz schrieb:> Einerseits bist du sehr genau und pedantisch was die Umsetzung der> Standards betrifft. Dann machst du aber Vorschläge die darauf> hinauslaufen:>> wo double draufsteht ist aber nicht double drin!!??
Wir haben offensichtlich verschiedene Anforderungen. Du willst auf
Biegen und Brechen einen IEEE float/double zwischen den Geräten
verschicken. Darum gehts aber im Artikel nicht. Sondern welche
Möglichkeit man hat, wenn der Code auf jeder Plattform laufen soll. In C
gibt es eben leider keine Funktion float_to_ieee_754_char_array() und
zurück. Mag sein, dass das manchmal praktisch wäre, gibts aber eben
nicht. So gesehen bin ich da sogar sehr pragmatisch: Ich nehme die
Möglichkeiten, die der C-Standard mitliefert.
Denn auch auf die Gefahr hin, mich zu wiederholen: Wozu muss man bitte
einen double mit voller Genauigkeit zwischen zwei Geräten verschicken?
Nenn mir mal eine sinnvolle Anwendung dafür.
Mir ist bewusst, dass es Algorithmen gibt, für die man gar nicht genug
Stellen für die Zwischenergebnisse haben kann, weil sie durch die vielen
Iterationen und große Dynamikbereiche Rundsfehler schnell stark
auswirken. Aber solche Algorithmen rechnet man doch nicht im
Ping-Pong-Verfahren mit zwei Geräten: Gerät A rechnet einen Schritt,
schickt die den Wert an B, B rechnet einen Schritt, schickt den Wert
zurück an A, A rechnet wieder einen Schritt ... Wenn man sowas baut, ja,
dann darf man beim Übertragung keine zusätzlichen Rundungsfehler
einbauen.
Die Situation in eingebetten Geräten ist dagegen: Datenquelle (z.B.
Sensor) -> Gerät A -> Gerät A berechnet einen Algorithmus -> schickt
Ergebnis an B -> B berechnet einen Algorithmus -> Datensenke (in Datei
speichern, am Bildschirm ausgeben, an Aktor schicken). Für einen
Datensatz hat man also einmal eine Übertragung. Wozu braucht die mehr
32 Bit Mantisse (+ Exponent)? Was für Ausgangswerte stecken da drinnen,
die so genau sind? Wenn es das in Spezialmessgeräten im Wert von
Einfamilienhäusen gibt, gut, dann überträgt man halt mehr. Aber in 99,9%
der für mich denkbaren Anwendungsfälle wäre das völliger Overkill.
Man muss sich sowieso ein Protokoll ausdenken, mit dem man seine Daten
verschickt. Warum sollte man darin also nicht festlegen, wie viele
Mantissenbits man für einen Datensatz braucht? Man muss das Protokoll
immer auf Sender und Empfänger implementieren, man hat also völlige
Wahlfreiheit. Und auf jedem Sender und Empfänger, für den es eine
standardkonforme C-Implementierung gibt, gibt frexp() und ldexp().
Übrigens auch printf(), scanf(), atof(), .... Integerverarbeitung
(Stichwort Festkomma) sowieso. Also bediene ich mich doch aus diesen
Möglichkeiten, statt irgendeinem IEEE-Standard hinterzuhecheln, den ich
nicht ohne Verrenkungen plattformübergreifend konvertiert bekomme. Falls
man auf einer Seite keine Wahlfreiheit hat, erübrigt sich die Diskussion
sowieso: Dann muss man des implementieren, was die Gegenseite will.
Und was ich auch schon mehrmals gesagt habe: Wenns Dir nicht drauf
ankommt, dass Code plattformunabhängig ist, sondern dass er schnell und
effizient ist, hält Dich weiterhin kein Mensch davon ab, Dir per
memcpy() die Bitrepräsentation des floats zu holen. Aber zu erwarten,
dass genau diese Variante, die plattformabhängig ist, im Artikel über
Plattformunabhängigkeit empfohlen wird, ist absurd. Das ist, wie wenn
man sich drüber beschwert, wenn in einem Artikel zu umweltbewussten
Verhalten empfohlen wird, mit dem Fahrrad zum Bäcker zu fahren. Das Auto
ist schließlich viel schneller und bequemer. Oder dass die
Autohersteller gefälligst Autos bauen sollen, die die Umwelt nicht
belasten.
Fabian O. schrieb:> Wir haben offensichtlich verschiedene Anforderungen. Du willst auf> Biegen und Brechen einen IEEE float/double zwischen den Geräten> verschicken.
Ja weil ich auf der versendenden Seite nicht jedes float/double
analysieren will wieviel Genauigkeit der Wert braucht, möglicherweise
muß ich auch einen Kollegen fragen was Sache ist. Der wird eher die
volle Genauigkeit verlangen, um nicht der Schuldige zu sein, wenn etwas
dann nicht funktioniert. Bei der Wiederverwendung des Codes muß man auch
mehr aufpassen. Sollte man aber auch einmal Zwischenergebnisse zu
Debugzwecken auf dem PC brauchen, ist es halt auch vorteilhafter die
originalen float oder double zu verwenden.
Fabian O. schrieb:> Denn auch auf die Gefahr hin, mich zu wiederholen: Wozu muss man bitte> einen double mit voller Genauigkeit zwischen zwei Geräten verschicken?> Nenn mir mal eine sinnvolle Anwendung dafür.
Mit voller Genauigkeit vielleicht nicht. Aber mit float kommt man schon
leicht nicht aus:
1.) Agilent hat ein Digitalvoltmeter mit 8,5 Stellen Auflösung. Wenn du
nun bedenkst, dass man intern gern mit einer Stelle mehr rechnet kommt
man schon reichlich über das float format hinaus.
2.) Die GPS-Zeit wird in usec beginnend 5. Januar 1980 um 24:00:00
angegeben, da kommt auch ein recht heftige Genauigkeit zusammen.
Du hast schon recht die vollen 52-bit werde ich kaum brauchen. Aber bei
weniger bit müßte man wahrscheinlich noch die Rundung beachten.
Fritz schrieb:> 2.) Die GPS-Zeit wird in usec beginnend 5. Januar 1980 um 24:00:00> angegeben, da kommt auch ein recht heftige Genauigkeit zusammen.
Warum in aller Welt sollte man die in einen double wandeln?
Einen float von einer Plattform auf die andere zu transportieren hat
doch zunächst nichts mit der plattformunanhängigkeit des Codes zu tun,
sondern damit, dass sich beide Seiten auf ein Protokoll, d.h. das Format
der zu übertragenden Daten festlegen.
Und noch einer schrieb:> Meine Erfahrung in der Praxis ist, dass man das in> nahezu allen Fällen nicht braucht.
Du scheinst keine Erfahrung mit nachhaltiger Entwicklung zu haben.
Im Automotive-Bereich wird alle paar Jahre die ganze Software auf die
nächste Generation portiert.
Gruß Anja