Hallo Freunde,
mein C-Buch hilft mir hier nicht weiter:
1
uint8_tcnt=0;
2
3
if(cnt+10<255)cnt++;
die Operation "cnt+10" ist ja ein 8-bit Wert plus 10. Was passiert also
bei bspw. cnt = 250? Kommt es schon in dem Vergleich zu einem überlauf
oder wird JEDE mathematische Opaeration im GCC auf uint16_t gecastet?
Wird auch auf signed int16_t gecastet? Sprich bei dem Fall:
Das Ergebnis von ganzzahligen Rechnungen ist per C-Definition so, als
wäre es mindestens mit "int" gerechnet worden. In diesen Beispielen wird
mit "int" gerechnet.
Achtung: GCC ist korrekt, aber nicht alle Compiler für 8-Bit
Mikrocontroller halten sich an diese Regel. Manche muss man explizit
davon überzeugen, sich nach dem Standard zu richten.
Also egal an welcher Stelle auch immer ich eine Rechenoperation
durchführe castet der Compiler auf 16 bit?
Ist er signed oder unsigned? (Logischer wäre sicherlich signed)
Phil schrieb:> Also egal an welcher Stelle auch immer ich eine Rechenoperation> durchführe castet der Compiler auf 16 bit?
Er konvertiert alle char/short Werte, die in "int" passen, nach "int",
sonst nach "unsigned".
> Ist er signed oder unsigned? (Logischer wäre sicherlich signed)
Da sich alle Werte von uint8_t vollständig in "int" wiederfinden, wird
in "int" gerechnet, also mit Vorzeichen.
Dann geh ich mal eine Stufe weiter.
Ich rufe eine Funktion count auf:
1
voidcount(uint8_tval){
2
if(val>255)x=255;
3
elsex++;
4
}
5
//global
6
uint8_tx=250
7
8
main(){
9
count(x+25);
10
}
was passiert hierbei? Wenn das Argument der Funktion übergeben wird,
wird dann erstmal in 16bit gerechnet. Der Wert dann in eine 8bit
variable gepresst und dann erst der Vergleich geführt?
Wenn ihr das Problem versteht. Wie kann ich den Überlauf umgehen? "val"
als int16_t zu definieren fällt flach.
Phil schrieb:> Wenn ihr das Problem versteht.
Nicht ganz.
1
voidcount(uint8_tval){
2
if(val>255)x=255;
3
elsex++;
val hat einen Wertebereich von 0..255. Der Vergleich ist damit sinnlos,
und er Optimizer wird den rauswerfen. Wenn du nicht möchtest, das val
"größer als 255" wird, d.h. nicht überläuft, brauchst du sowas hier:
Phil schrieb:> Dann geh ich mal eine Stufe weiter.>> Ich rufe eine Funktion count auf:>>
1
>voidcount(uint8_tval){
2
>if(val>255)x=255;
3
>elsex++;
4
>}
5
>//global
6
>uint8_tx=250
7
>
8
>main(){
9
>count(x+25);
10
>}
11
>
12
>
> was passiert hierbei? Wenn das Argument der Funktion übergeben wird,> wird dann erstmal in 16bit gerechnet.
Es wird integerbreit x+25 gerechnet und das Ergebnis für den
Funktionsaufruf mundgerecht auf 8 Bit gestutzt. Der Compiler wird den
oberen Zweig der Bedingung wegoptimieren und count wird immer x++
ausführen.
> Wenn ihr das Problem versteht. Wie kann ich den Überlauf umgehen? "val"> als int16_t zu definieren fällt flach.
Vorher schauen, obs überlaufen wird.
Warum fällt int16_t flach?
Phil schrieb:> if(val > 255) x = 255;
Sinnlos, da val nie > 255 werden kann.
> else x++;
Kann überlaufen und dann steht 0 in x.
> count(x + 25);
Da der Parameter als uint8_t deklariert ist, wird (uint8_t)((int)x + 25)
übergeben, also 19.
Merke: Nur Zwischenrechnungen werden expandiert. Wird das Ergebnis einer
solchen Rechung einer Variable zugewiesen, dann enthält diese Variable
nur das, was dort reinpasst.
> Wenn ihr das Problem versteht. Wie kann ich den Überlauf umgehen? "val"> als int16_t zu definieren fällt flach.
Du hast ein paar abstrakte Beispiele genannt, nicht aber das eigentliche
Problem.
Phil schrieb:> was passiert hierbei?
Gehs der Reihe nach durch
Du hast zunächst mal 2 Phasen
Vor dem Aufruf
Nach dem Aufruf
Vor dem Aufruf muss passieren
Argumente auswerten
An die tatsächlich von der Funktion gewünschten Datentypen
anpassen
Du hast
count(x + 25);
x ist ein uint8_t mit dem Wert 250
Um die Addition zu machen wird aus dem uint8_t ein int
250 + 25 macht 275
Damit sind die Argumente evaluiert.
Der eigentliche Aufruf:
Du möchtest den Wert 270 an eine Funktion übergeben, die einen uint8_t
nimmt. Das geht erst mal so nicht. Von dem int wird das obere Byte
gestrippt, aus 270 wird so 19
Damit bist du innerhalb der Funktion:
if(val > 255) x = 255;
das ist natürlich selten dämlich. val ist ein uint8_t. Den uint8_t
möchte ich sehen, der Werte größer als 255 annehmen kann :-)
Aber seis drum.
Die Funktion bekommt 19 als Wert für val. Und damit wird
weitergearbeitet.
> Wie kann ich den Überlauf umgehen? "val"> als int16_t zu definieren fällt flach.
Welchen Überlauf. Innerhalb der Funktion findet kein Überlauf statt.
Dein 'Problem' welches es auch immer konkret ist, ist nicht innerhalb
der Funktion zu lösen.
Phil schrieb:> Wenn das Argument der Funktion übergeben wird,> wird dann erstmal in 16bit gerechnet.
Nein, nicht unbedingt. Wie schon andere Diskussionsteilnehmer
geschrieben haben, erfolgt die Berechnung mit einem internen Datentyp,
dessen Breite einem "int" oder "unsigned int" entspricht.
Da Du es offenbar nicht nötig hast, hier auch nur ansatzweise
Andeutungen über das Zielsystem zu machen, kann keiner von uns wissen,
wie breit dort ein "int" ist.
In den meisten Fällen entspricht ein "int" der natürlichen
Datenwortlänge des Prozessors. Bei x86-Prozessoren gilt dies aber häufig
nicht, z.B. bei Anwendungen, die im DOS-Kompatibilitätsmodus für Windows
übersetzt werden und daher 16Bit-Integer haben.
Aber auch bei 64Bit-Systemen erlebt man die folgende Überraschung. Ich
empfehle, das folgende Progrämmchen jeweils mittels
gcc -m32
und
gcc -m64
zu übersetzen und auszuführen.
Bei 64-Bit kann man dazu munkeln:
Wenn man den int auf 64 Bit aufgeblasen hätte, eben so, wie man es auch
eigentlich erwartet hätte, da ja jetzt 64 Bit die natürliche Wortbreite
der Maschine ist, so würden unzählige Programme auf einmal doppelt so
viel Arbeitsspeicher verbrauchen.
Nämlich immer dann, wenn im Programm ein int verbaut ist...
Aber so viel konsequentes 64 Bit wollen die Privatkonsumenten dann doch
nicht. Lieber 64 Bit können, aber dann wieder auf 32 Bit kastrieren.
Andreas Schweigstill schrieb:> Aber auch bei 64Bit-Systemen erlebt man die folgende Überraschung. Ich> empfehle, das folgende Progrämmchen jeweils mittels
Insbesondere unter Windows wird die Überraschung gross ausfallen. Unter
Linux/Unix ist's nicht ganz so arg.
Sven P. schrieb:> Aber so viel konsequentes 64 Bit wollen die Privatkonsumenten dann doch> nicht. Lieber 64 Bit können, aber dann wieder auf 32 Bit kastrieren.
Das hat nichts mit "Privatkonsumenten" zu tun. Der wahre Grund dürfte
eher darin liegen, dass Unmengen an Quelltexten auf Seiteneffekte hin
überprüft werden müssten.
Werden Dateien von "Altsystemen" mit 32Bit übernommen, müssen diese
natürlich lesbar bleiben. Ebenso gibt es Unmengen an
Netzwerkprotokollen, die implizit auf 32Bit-Datentypen basieren und
damit sehr genau kontrolliert werden müssten.
Die Beibehaltung der 32Bit-Integer hat also eher etwas mit
Bestandsschutz zu tun, und das völlig zu Recht!
Dummerweise handelt man sich dadurch wieder jene Probleme ein, mit denen
man schon anno 16-Bit x86 zu kämpfen hatte. Nämlich der leider immer
noch gelegentlich anzutreffenden Annahme, dass ein "int" oder "unsigned"
oder - bei vorsichtigeren Gemütern - zumindest ein "long" eine
Speicheradresse gefahrlos transportieren kann.
Dazu kommt ein weiteres und neues Problem: Der Datentyp für
Array-Indizierung lässt sich in C nicht deklarieren, nicht zuletzt
aufgrund der definierten Äquivalenz von Pointerrechung und
Array-Indizierung. Wie der Compiler damit nun umgehen soll ist aus
Quelltextsicht somit eher schwarze Magie als klare Sache, bzw. vom
Datentyp der Indexoperanden abhängig.
Ich wäre also nicht überrascht, wenn mit der Verbreitung von Dataspaces
grösser als 4GB ein Index-Modulo-Problem eine Rolle ähnlich dem
Buffer-Overflow bekommt.
Andreas Schweigstill schrieb:> Nein, nicht unbedingt. Wie schon andere Diskussionsteilnehmer> geschrieben haben, erfolgt die Berechnung mit einem internen Datentyp,> dessen Breite einem "int" oder "unsigned int" entspricht.
Das ist etwas seltsam formuliert. Die Integer-Promotion konvertiert den
Wert zunächst nach "int" oder "unsigned int" und rechnet dann mit dem
Ergebnis dieser Konvertierung. Die Berechnung erfolgt also formal mit
exakt diesem Typ und nicht nur mit irgendeinem internen Typ derselben
Größe.
> Bei x86-Prozessoren gilt dies aber häufig nicht, z.B. bei Anwendungen,> die im DOS-Kompatibilitätsmodus für Windows übersetzt werden und daher> 16Bit-Integer haben.
Naja, diese Programme laufen aber dann auch im 16-Bit-Modus, so daß man
im Prinzip schon sagen kann, daß die natürliche Breite in dem Fall 16
Bit ist. Sonst müßte konsequenterweise auf einem Core2-Prozessor ein int
64 Bit groß sein, auch wenn darauf gerade ein 32-Bit-Betriebssystem
läuft.
> Aber auch bei 64Bit-Systemen erlebt man die folgende Überraschung.
Überraschend ist es eigentlich nicht. Das Problem ist, daß einem hier
einfach die Typen ausgehen. Wäre int 64 Bit breit, wären nicht mehr
genug Standard-Typen für 8, 16 und 32 Bit übrig, weil es nur zwei gibt,
die kleiner als int sein dürfen.
Rolf Magnus schrieb:> Überraschend ist es eigentlich nicht. Das Problem ist, daß einem hier> einfach die Typen ausgehen.
Andererseits gehen einem dank Microsofts weisem Ratschluss nun am
anderen Ende die Typen aus. Denn in ANSI-C ('89/'90) ist es auf einem
64-Bit Windows unmöglich, mit 64 Bit Integers zu arbeiten. "long" steht
für 32 Bits und "long long" gibt es offiziell erst in C99.
Andreas Schweigstill schrieb:> Die Beibehaltung der 32Bit-Integer hat also eher etwas mit> Bestandsschutz zu tun, und das völlig zu Recht!
Meine Meinung:
Nein, das ist totaler Blödsinn! :-)
Dort, wo explizite Wortlängen gefordert sind, sollen sie auch explizit
vereinbart werden. Nicht nur deshalb hat man vor Äonen mal sowas wie
stdint.h eingeführt.
Quelltext, der von der Breite von int & Konsorten abhängt, ist einfach
für die Tonne.
Solcherlei Quelltexte sind MURKS von Anfang an, seit es z.B. stdint.h
gibt. Sowas gehört bedingungslos in ein ordentliches Review, denn früher
oder später ist ein int 64 Bit breit.
Immerhin hätte ich (aber auf mich hört ja niemand G) wenigstens beim
Sprung auf 64 Bit mal eine ganze Reihe von Altleichen beseitigt. Die
meisten Betriebssysteme (vermutlich abgesehen von Windows) wären nicht
dran krepiert, dort muss seit eh und je schon mehrgleisig (x86, ARM,
...) gefahren werden. Aber dafür wären solche Leichen wie die Timer, der
verkappte PIC, MMX/SSE/Trallalla etc. endlich mal aus dem Keller.
Die Anwendungen werden sowieso für jede Distribution neu übersetzt.
Aber das genau ist ja so ein Kernproblem. Jede Klitsche ist geil auf
ihre supergeheimen Quelltexte. Ist die Klitsche mal pleite, steht wieder
jemand mehr auf dem Schlauch, weil die Software auf irgendwelche total
veralteten Dinge besteht.
Normalerweise würde man jetzt einfach das Programm entsprechend anpassen
(lassen) neu auf der Zielarchitektur übersetzen und gut wärs. Aber nein,
da wird z.B. ein Counterstrike-Server ausgeliefert, der unbedingt SSE2
benutzen möchte und kein Fallback hat. Is natürlich für ältere AMDs
optimal, illegal instruction und Ende.
Sven P. schrieb:> Meine Meinung:> Nein, das ist totaler Blödsinn! :-)
Nein, das ist einfach Realität.
> Dort, wo explizite Wortlängen gefordert sind, sollen sie auch explizit> vereinbart werden. Nicht nur deshalb hat man vor Äonen mal sowas wie> stdint.h eingeführt.
Das heißt aber noch nicht, dass dies konsequent umgesetzt wurde.
Außerdem fällt dies ja frühestens dann erst auf, wenn ein Wechsel auf
eine neuere Generation von Prozessoren/Betriebssystem ansteht. Und dann
ist es zu spät, die alten Fehler zu korrigieren.
> Quelltext, der von der Breite von int & Konsorten abhängt, ist einfach> für die Tonne.
Das mag ja sein, aber trotzdem ist man auf solche Software auch auf dem
neuen System angewiesen.
> Solcherlei Quelltexte sind MURKS von Anfang an, seit es z.B. stdint.h> gibt. Sowas gehört bedingungslos in ein ordentliches Review, denn früher> oder später ist ein int 64 Bit breit.
Es gibt keinerlei Verpflichtung, solch ein Review durchzuführen. Wer
soll das denn auch bezahlen? Wenn der Kunde eine Software haben will,
die auf Betriebsystem A Version B läuft, dann bekommt er diese. Sofern
nichts anderes vereinbart ist, bezahlt er auch nur diese Version.
> Immerhin hätte ich (aber auf mich hört ja niemand G) wenigstens beim> Sprung auf 64 Bit mal eine ganze Reihe von Altleichen beseitigt.
Die Welt ist voll von solchen realitätsfremden Weltverbesserern.
> Die Anwendungen werden sowieso für jede Distribution neu übersetzt.
Nein. Das mag zwar auf viele Open-Source-Projekte zutreffen, aber für
den Großteil kommerzieller Software gilt dies nicht. Kompatibilität wird
meist nur für eine bestimmte Distribution zugesagt, im Linux-Bereich
häufig irgendeine leicht angegraute Redhat-Version.
> Aber das genau ist ja so ein Kernproblem. Jede Klitsche ist geil auf> ihre supergeheimen Quelltexte.
Diese "supergeheimen Quelltexte" sind die Geschäftsgrundlage der meisten
Klitschen.
Hast Du schon einmal versucht, selbst Geld zu verdienen? Ich gehe nicht
davon aus.
> Ist die Klitsche mal pleite, steht wieder> jemand mehr auf dem Schlauch, weil die Software auf irgendwelche total> veralteten Dinge besteht.
Zu dem Zeitpunkt, als die Software auf den Markt kam, waren die
Voraussetzungen aber vermutlich nicht veraltet. Und der Kunde hat die
Software ja auch nur für den damaligen Anwendungszweck gekauft. Ggf. hat
er einen Wartungsvertrag für die Lieferung von Software-Updates erworben
und damit vielleicht auch Software für neuere Betriebssystemversionen
erhalten.
> Normalerweise würde man jetzt einfach das Programm entsprechend anpassen> (lassen) neu auf der Zielarchitektur übersetzen und gut wärs.
Nicht normalerweise, sondern idealerweise. Die Realität definiert, was
normal ist und nicht was idealerweise gelten sollte.
Andreas Schweigstill schrieb:> [...]> Hast Du schon einmal versucht, selbst Geld zu verdienen? Ich gehe nicht> davon aus.
Ja, hat auch funktioniert.
Im Ernst, ich stimme dir ohne Vorbehalte zu; meine Aussagen waren schon
bewusst etwas überspitzt, ganz so naiv bin ich dann doch nicht.
Dass das praktisch nicht bzw. nicht mehr umzusetzen ist, weiß ich auch.
Aber man sieht, wie m.M.n. closed-source das Krebsgeschwür der
Computerwelt ist, um es mal mit nicht ganz eigenen Worten zu sagen. Es
hemmt den Fortschritt ganz gewaltig, da andauernd irgendwelche Altlasten
mitgeschleppt werden.
Irgendwann kommt der Sprung weg von diesen Leichen im Keller, ob man es
nun für wahr haben möchte oder nicht. Also besser jetzt Review machen
und sauber arbeiten, als später wieder mal aufs Maul zu fallen, oder?
Hier (Linux) hat man das z.B. erkannt, und htons() und Konsorten auf
explizite Breiten umgestellt. Windows benutzt in den Winsock-Headern
bislang u_short und u_long, wobei sich die MSDN darüber ausschweigt, ob
ein u_short dasselbe ist, wie ein ushort, welches nämlich ein unsigned
short ist. Oder ob u_short ur u_short heißt, aber in Wahrheit ein
uint16_t ist.
Sehr durchdacht :-)
Es würde vermutlich auch niemand etwas sagen, wenn es das erste Mal
wäre, dass sowas passiert. Aber man ist ja in der nahen Vergangenheit
schon mehrmals über denselben Mist gefallen -- OS-Hersteller wie
Klitschen.
Wer da nun nicht schlau draus geworden ist, der ist wohl selbst schuld.
Dumm ist nur, dass wieder tausend andere darunter leiden müssen, weil
z.B. irgendeine Software nicht mehr wartbar ist und man deshalb an Win95
klebt. -- produktiv geht irgendwie anders.
Sven P. schrieb:> Dort, wo explizite Wortlängen gefordert sind, sollen sie auch explizit> vereinbart werden. Nicht nur deshalb hat man vor Äonen mal sowas wie> stdint.h eingeführt.
Als die ersten 64bit-Architekturen eingeführt wurden, war es noch ein
weiter Weg bis C99.
Unter anderem auf den damals gewonnenen Erfahrungen beruht es wohl, dass
überhaupt die Notwendigkeit für eine "stdint.h" erkannt wurde.
> Quelltext, der von der Breite von int & Konsorten abhängt, ist einfach> für die Tonne.> Solcherlei Quelltexte sind MURKS von Anfang an, seit es z.B. stdint.h> gibt.
Es gibt Tonnen von bis heute verwendetem Code, der erheblich älter als
C99 ist. Soviel zum Thema "MURKS von Anfang an". Ausserdem verschwindet
Code nicht, bloss weil er vielleicht Murks (oder eher: nicht mehr
zeitgemäss) ist.
Wenn der jahrzentelang funktionierende Code auf einmal auf einer neuen
Maschine nicht mehr funktioniert, dann ist für den Kunden die neue
Maschine schuld, nicht der Code. Wie es vielleicht technisch oder
akademisch aussieht, interessiert ihn da einen Scheissdreck.
> Sowas gehört bedingungslos in ein ordentliches Review, denn früher> oder später ist ein int 64 Bit breit.
Korrekt wäre "früher war". Die allerersten 64bit-Systeme haben int auf
64bit aufgedreht (ILP64-Modell). Später hat man erkannt, dass das nicht
wirklich günstig ist, und ist zum LP64-Modell gewechselt (und MS jetzt
zu LLP64). Der Erhalt eines "natürlichen" C-Typs mit 32bit hat dabei
aber nur eine kleine Rolle gespielt, es gibt noch diverse andere Gründe.
http://www.unix.org/version2/whatsnew/lp64_wp.html> Immerhin hätte ich (aber auf mich hört ja niemand G) wenigstens beim> Sprung auf 64 Bit mal eine ganze Reihe von Altleichen beseitigt.
Schau dir mal die Geburtswehen von DEC Alpha als erste 64bit-Architektur
im Opensource-Bereich an. Es hat Jahre gedauert, bis die DEC
Alpha-Versionen der Linux-Distributionen den gleichen Umfang erreicht
hatten wie die x86-Versionen. Eines der Hauptprobleme hierbei war, dass
auf einmal long und int nicht mehr die gleiche Größe hatten.
Natürlich war der betroffene Code unsauber geschrieben, das Problem ist
aber, dass Fehler ohne jede Auswirkung auf die Programmlogik (und um
einen solchen handelt es sich, wenn bei 32bit-int und -long nicht sauber
zwischen beiden unterschieden wird) bei den üblichen
Entwicklungsprozessen nicht entdeckt werden.
Ich vermute, dass eben diese Geburtswehen auch der Grund sind, warum
Microsoft bei 64bit-Windows long bei 32bit belassen hat. Die Probleme
bei der Umstellung sind schon groß genug, da wollten sie dieses Fass
nicht auch noch aufmachen. Allerdings bricht das dann mit einer anderen
häufig zu findenden Annahme, nämlich dass ein long gross genug ist, um
einen Pointer aufzunehmen.
Andreas
Sven P. schrieb:> Also besser jetzt Review machen> und sauber arbeiten, als später wieder mal aufs Maul zu fallen, oder?
Das kann man nicht pauschal beantworten.
Das Review müsste für jede einzelne Anwendung geschehen, wohingegen
beim Bereitstellen einer Ausführungsumgebung für Altanwendungen das
Problem gleich für sehr viele Anwendungen gelöst wird.
Und selbst wenn der Quellcode vorliegt, kann man nicht "einfach 'mal so"
neu kompilieren und dabei alle Probleme beheben. Wir sehen es ja im
Linux-Bereich, wo zwar für den meisten Kram der Quellcode vorliegt, aber
sowohl das Neukompilieren von Altanwendungen auf aktuellen
Distributionen als auch das Kompilieren neuer Anwendungen auf alten
Distributionen ist ja oft mit erheblichem Aufwand verbunden.
Ich hatte gerade vor ein paar Wochen das Problem, eine neue Version von
Amanda auf einem openSUSE 10.2 zum Laufen zu bringen. Es ist mir bisher
nicht gelungen, da eine wesentliche Bibliotheksabhängigkeit nicht
aufgelöst werden kann. Die Bibliothek ebenfalls upzudaten würde einen
unglaublichen Rattenschwanz an Updates nach sich ziehen.
Obwohl ich Linux-Aktivist der ersten Stunde und auch Kernelentwickler
bin, habe ich von der o.a. Aktion Abstand genommen.
Gerade die Open-Source-Thematik führt meines Erachtens genau zu dem
Effekt, dass sich Entwickler zu wenig Gedanken um Altsysteme machen,
sondern sich darauf verlassen, dass der Anwender den Kram auf dem
Altsystem neu kompilieren kann/muss. Damit ist das System ziemlich ad
absurdum geführt.
Da die Zahl von Softwarepaketen für Linux immer weiter ansteigt, sind
auch die Distributionsanbieter nicht mehr in der Lage, all die Pakete
neu zu kompilieren und einer Qualitätskontrolle zu unterziehen.
Somit sind wir ein sehr, sehr großes Stück davon entfernt, den
gewünschten Idealzustand zu erreichen.
Auch darf man nicht vergessen, dass die unter Linux am meisten genutzten
APIs auch schon zwanzig und mehr Jahre alt sind. Dateizugriffe basieren
in den allermeisten Fällen auch nur auf dem tradierten
User/Group/Other-Konzept und nicht auf ACLs und ähnlichen Mechanismen.
Der Open-Source-Charakter hat da keineswegs zu einer durchgängigen
Modernisierung geführt, eben weil die Umstellungen von Anwendungen sehr
aufwändig und mit unüberschaubaren Seiteneffekten verbunden sein können.
Aber weshalb z.B. hält sich besagtes Problem in den Winsock-Routinen
dann so hartnäckig? Ebendort wäre es doch problemlos möglich, statt
ushort einen uint16_t zu verbauen, ohne auch nur irgendetwas zu
verändern.
Dennoch findet man dort heute noch diesen Murks. Das hätte man doch z.B.
in einem Zug mit der Umstellung auf IPv6 erledigen können?
Und danke für die vielen Erläuterungen, da waren (wie erwartet) jede
Menge Aspekte dabei, die für mich als Hobbyist eher nebensächlich waren
oder von denen ich eigentlich nicht erwartet hätte, dass sie derart
sperrig sind.
Sven P. schrieb:> Und danke für die vielen Erläuterungen, da waren (wie erwartet) jede> Menge Aspekte dabei, die für mich als Hobbyist eher nebensächlich waren> oder von denen ich eigentlich nicht erwartet hätte, dass sie derart> sperrig sind.
Du bist ja gerade dabei, einen 80-Bit Taschenrechner zu stricken.
Versuch doch einfach mal, den Code so zu schreiben, daß er auf 8-, 16-
und 32-Bit Systemen sowohl lauffähig als auch nicht unnötig ineffizient
ist. Du wirst feststellen, daß dein Code nicht gerade hübscher wird.
Als erstet fliegen die uint8_t für Schleifen rausraus, weil der auf
32-Bit Systemen sehr ungünstig sind. Ok, dafür gibt es den uint_fast8_t,
den du stattdessen verwenden solltest. Wie machst du es mit dem
Speicher? Hier sind 32-Bit Werte besser als 8-Bit Werte. Also möchtest
du auch da vermutlich den uint_fast8_t verwenden. Ok. Wieviel
Arrayelemente brauchst Du? Dafür bemühen wir sizeof -- doch stop,
verschissen. 80 geht nicht durch 32. Also nehmen wir ein uint16_t, der
schon die ersten Abstriche in der Performance bring, etwa bei
Multiplikation. Das Ergebnis ist ein uint32_t -- oder doch lieber ein
uint_fast32_t was evtl ein 64-Bit Wert ist...?
Nächste Hürde sind die impliziten Casts in C. Die C Spez bezieht sich
idR auf int bzw. unsigned int. Um unangenehme Nebeneffekte
auszuschliessen beginnst Du bei jedem Ausdruck ne Cast-Orgie.
Nächstes Thema: Endianess...
Nächstes Thema: Alignment in Structs/Unions, Alignments von
Speicherzugriggen (auf AVR kein Thema) ...
Nächstes Thema: Pointer-Casts
etc, etc.
Klar, dein Projekt machst du nur für AVR, und auch nur für eine
bestimmte Sorte oder sogar nur für einen bestimmten µC. Aber du siehst,
wie schnell das Kreise zieht und Aufwand erzeugt.
Vor allem auch für Plattformen, auf denen dein Code eh nie laufen wird
-- oder doch?
Hm, das wird offenbar furchtbar kompliziert, zweifellos.
Wie sieht es denn aber aus, wenn es darum geht, Standards umzusetzen,
wie es bei dem htons() & Co. der Fall ist?
In solchen Fällen dürfte die Lage doch eindeutig sein, oder?
Zu den Cast-Orgien: Die sind wirklich eine Qual. Auch da: Strenggenommen
sind die doch Pflicht -- alles andere ist wieder Murks und Kaffeesatz?!
Immerhin weiß ich immer mehr darum, PCs nicht zu mögen :-)
Johann L. schrieb:> Als erstet fliegen die uint8_t für Schleifen rausraus, weil der auf> 32-Bit Systemen sehr ungünstig sind.
Wobei GCC sich einen Dreck drum schert, denn dessen Erkennung solcher
Schleifenmuster ersetzt den Zähler sowieso durch int, in der sicheren
Gewissheit, das wäre der effizienteste Datentyp. Weshalb AVRs gekniffen
sind, denn so zählt er leider in 16 Bits statt 8 Bits obwohl
ausdrücklich ein 8 Bit Typ verwendet wurde.
A. K. schrieb:> Wobei GCC sich einen Dreck drum schert, denn dessen Erkennung solcher> Schleifenmuster ersetzt den Zähler sowieso durch int, in der sicheren> Gewissheit, das wäre der effizienteste Datentyp. Weshalb AVRs gekniffen> sind, denn so zählt er leider in 16 Bits statt 8 Bits obwohl> ausdrücklich ein 8 Bit Typ verwendet wurde.
Kann ich aber nicht bestätigen. Ich benutze avr-gcc 4.3.5 und habe
einige Schleifen mit int8_t-Zählvariablen, die er auch allesamt brav in
ein einzelnes 8-Bit-Register packt.
An anderer Stelle verkorkst er dafür aber. Statt einfach am
Schleifenende (Schleife zählt rückwärts) ein 'dec' + 'brne' zu setzen,
dekrementiert er den Zähler zu Beginn, rechnet den Schleifenkörper,
prüft dann mit 'cpi' den Zähler und springt dann erst :-/
Sven P. schrieb:> Kann ich aber nicht bestätigen.
War ich grad eben drüber gestolpert, als ich den Vektorcode aus deinem
anderen Thread durch GCC jagte. Kam
cp r30,r22
cpc r31,r23
brne .L2
bei raus, trotz uint8_t als Zähler. avr-gcc 4.3.4, -O1.
A. K. schrieb:> Johann L. schrieb:>>> Als erstet fliegen die uint8_t für Schleifen rausraus, weil der auf>> 32-Bit Systemen sehr ungünstig sind.>> Wobei GCC sich einen Dreck drum schert, denn dessen Erkennung solcher> Schleifenmuster ersetzt den Zähler sowieso durch int, in der sicheren> Gewissheit, das wäre der effizienteste Datentyp.
Da ist bestenfalls für einfache Schleifen der Fall. Bei komplizierterem
Code wäre es viel zu aufwändig, die möglichen Werte der Variablen zu
bestimmen bzw. ist nicht mehr möglich. Value range propagation et al.
haben eben auch ihre Grenzen, und hier ist es woe so oft: Wenn der
Programmierer mehr wissen hat als der Compiler hat (oder in der Lage ist
herauszufinden), ist das Ergebnis suboptimal.
GCC schert sich nicht "einen Dreck" um solche Datentypen. Er beachtet
deren Semantik genau und setzt das Programm danach um in IL. Die
Optimierungen kommen erst danach, und müssen (oder sollen) alles
rausfischen, was ineffizient ist, was eben noch nicht immer perfekt
gelingt.
Kann natürlich gut sein, wenn du mit O1 (also auf Speed) optimierst. Da
der Compiler ja denkt, dass es "schneller" geht. Üblicherweise
kompiliert man ja mit -Os, wo er wahrscheinlich dann die Erweiterung auf
16 Bit sein lässt, da das mehr Platz braucht als ein 8Bit Zähler (der
ja au ausreicht).
Johann L. schrieb:> GCC schert sich nicht "einen Dreck" um solche Datentypen. Er beachtet> deren Semantik genau und setzt das Programm danach um in IL.
Schon klar, keep cool ;-).
Nur entbehrt es nicht einer gewissen Ironie, wenn der für AVR optimierte
und auf 32-Bittern suboptimale aussehende Quelltext ausgerechet auf den
16/32-Bittern effizient und auf dem AVR leicht ineffizient ist.
Simon K. schrieb:> Kann natürlich gut sein, wenn du mit O1 (also auf Speed) optimierst. Da> der Compiler ja denkt, dass es "schneller" geht. Üblicherweise
-Os und -O2 liefern zwar einen anderen Code, der ist aber ebenso
umständlich.
Korrektur: Der Zähler fliegt ganz raus, also nicht "int" wie ich oben
schrieb, aber es wird einer der laufenden Pointer verglichen:
ldi r24,hi8(vector0+10)
cpi r30,lo8(vector0+10)
cpc r31,r24
brne .L2
Dummerweise ist das schlechter als ein separater 8-Bit Zähler, erst
recht wenn dekrementierend.
Man muß hier aber auch bedenken, daß gcc eigentlich gar nicht für
8-Bit-Prozessoren gemacht wurde und daß avr ein verhältnismäßig selten
verwendetes Target ist. Es gibt nicht viele avr-Programmierer, die Lust
haben und sich zutrauen, am gcc mitzuentwicklen und dort Optimierungen
zu implementieren. Entsprechend bekommt der avr-Zweig im Vergleich zu
Architekturen wie ARM oder x86 recht wenig Aufmerksamkeit.
Man muß hier aber auch bedenken, daß gcc eigentlich gar nicht für
8-Bit-Prozessoren gemacht wurde und daß avr ein verhältnismäßig selten
verwendetes Target ist. Es gibt nicht viele avr-Programmierer, die Lust
haben und sich zutrauen, am gcc mitzuentwicklen und dort Optimierungen
zu implementieren. Entsprechend bekommt der avr-Zweig im Vergleich zu
Architekturen wie ARM oder x86 recht wenig Aufmerksamkeit.
Klar, aber mir erscheint fraglich, ob man da viel dran drehen kann ohne
wesentlich beim maschinenübergreifenden Teil mitzumischen, der für
solche Optimierungen verantwortlich zeichnet. Der geht von bestimmten
Grundannahmen aus, und eine davon ist die Wortverarbeitung als
kostengünstigste Maschinenoperation. C ist schon als Sprache so
konzipiert, und es stimmt ja auch fast immer - aber 8-Bitter sind dabei
eben gekniffen.
Man muss also bei entsprechenden Anpassungen genau da reingreifen, wo
sich auch alle anderen GCC Maintainer angesprochen fühlen können, nicht
nur die für's AVR Target.
Soweit ich erkennen kann, scheint die Erweiterung des Schleifenzählers
auf 16bit (bzw. die Nutzung eines Pointers als Zähler) immer dann zu
passieren, wenn die Schleifenvariable als Index in ein Array benutzt
wird.
Vermeiden lässt sich das, indem stattdessen zusätzlich ein Pointer
mitgeführt und inkrementiert wird.
Statt
1
uint8_tfoo(constuint8_tdata[10])
2
{
3
uint8_tx=0;
4
5
for(uint8_ti=0;i<10;i++){
6
x|=data[i];
7
}
8
9
returnx;
10
}
also besser
1
uint8_tfoo(constuint8_tdata[10])
2
{
3
uint8_tx=0;
4
5
constuint8_t*p=&data[0];
6
for(uint8_ti=10;i>0;i--){
7
x|=*p++;
8
}
9
10
returnx;
11
}
Die erste Variante ergibt kompiliert mit -O2 bei mir 32 Byte Code, die
zweite nur 18 Byte.
Da die Schleifenvariable jetzt nicht mehr als Index benötigt wird, kann
ausserdem rückwärts gezählt werden, was (wenn die Schleife bei 0 endet)
auch noch den Vergleich am Schleifenende einspart (es können direkt die
Flags vom Dekrementieren der Variable mittels SUBI ausgewertet werden).
Interessanterweise stellt das zumindest bei dem konkreten Beispiel oben
übrigens noch nichtmal unbedingt eine Verschlechterung auf anderen
Architekturen dar, wie ich erwartet hätte. Erstaunlich, aber der
generierte Code auf x86 ist in beiden Fällen komplett identisch.
Andreas
Andreas Ferber schrieb:> Soweit ich erkennen kann, scheint die Erweiterung des Schleifenzählers> auf 16bit (bzw. die Nutzung eines Pointers als Zähler) immer dann zu> passieren, wenn die Schleifenvariable als Index in ein Array benutzt> wird.
Ich bin Deiner Angabe nachgegangen und habe in meinem AVR-Source solche
Stellen mal rausgesucht. Die Ersetzung des Array-Zugriffs auf
Pointer-Zugriff hat durchgehend eine Ersparnis von ca. 20 Bytes pro
Schleife gebracht.
Bisher dachte ich, der gcc wirds schon richten, da kommt derselbe Code
raus - egal ob ich einen Array- oder Pointerzugriff mache. Das ist auch
größtenteils auf 32-Bit-Plattformen so. Aber niemals hätte ich gedacht,
dass der gcc eine 16-Bit-Indexvariable "einschleust", wenn ich diese
doch explizit als uint8_t definiere.
Gruß,
Frank