Ich programmiere seit Jahren AVR in ASM. Zwischendurch hab ich mal
Bascom und Luna versucht, bin da aber immer recht schnell wieder
abgesprungen. Nun soll man ja seine Vorurteile immer mal gegenprüfen,
also hab ich mit C angefangen. Zum Einstieg hab ich ein in ASM
begonnenes Projekt nochmal in C aufgebaut. Allerdings macht der Compiler
mitunter komische Sachen, und ich würde gern verstehen, warum.
Z.B.:
Da soll also aus zwei im Array Rxtext liegenden Chars ein packed BCD
zusammengesetzt werden, für eine RTC. Wie ich es in ASM gewohnt bin,
schiebe ich das erste Zeichen um 4 bit und hänge das zweite Zeichen
dran.
Warum zum Henker will der Compiler hier plutizipieren? In ASM würde man
ein einfaches SWAP machen und mit 0xF0 maskieren.
Komischerweise macht er in der umgekehrten Operation, also packed BCD zu
zwei Chars genau das SWAP:
1
data=((rtcmm>>4)&0x0F)+'0';// Zehner zu BCD
2
a6c:80916301ldsr24,0x0163
3
a70:8295swapr24
4
a72:8f70andir24,0x0F;15
5
serial_write(data);
6
a74:805dsubir24,0xD0;208
7
a76:0e94f903call0x7f2;0x7f2<serial_write>
8
data=(rtcmm&0x0F)+'0';// Einer zu BCD
9
a7a:80916301ldsr24,0x0163
10
a7e:8f70andir24,0x0F;15
Warum macht der Compiler das? Ich möchte das gern verstehen.
BTW: Für ASM vs. C Kriege bitte den passenen Thread im Offtopic nutzen.
be s. schrieb:> warning: conversion to ‘char’ from ‘int’ may alter its value> [-Wconversion]
hast du Rxtext als char definiert? Was passiert bei unsigned char?
Ja sorry, war klar das wieder Info fehlt: Das Array ist als char und hh
bzw. data sind als uint8_t definiert. Und bei mir gibt es keine
Compilerwarnung.
Peter II schrieb:> char ist blöd. Denn es kann auch signed sein, dann ist das shift> undefiniert.
vergiss es, du rechnest ja mit -'0' - char ist also ok
Peter II schrieb:> hast du Rxtext als char definiert?
Alles char.
Peter II schrieb:> Was passiert bei unsigned char?
Gleiches Ergebnis.
Sogar das Casten aller Ausdrücke ändert nichts am Ergebnis:
Timm T. schrieb:> Warum zum Henker will der Compiler hier plutizipieren? In ASM würde man> ein einfaches SWAP machen und mit 0xF0 maskieren.>> Komischerweise macht er in der umgekehrten Operation, also packed BCD zu> zwei Chars genau das SWAP:
In C gilt bekanntlich, dass eine Berechnung mindestens in "int" zu
erfolgen hat, oder zumindest das Ergebnis dem entsprechen sollte. Da <<
auf 8 Bits nicht in den 8 Bits bleibt ist - lokal auf diese Operation
bezogen - eine 8-Bit Rechnung nicht zulässig. Tatsächlich ist diese
Multiplikation der effizienteste Weg, genau dies zu tun.
Das geht erst durch die (vermutlich) anschliessende Zuweisung an eine
8-Bit Variable wieder nach hinten los, aus der er schliessen könnte,
dass es bei den 8 Bits bleibt.
Peter II schrieb:> char ist blöd. Denn es kann auch signed sein, dann ist das shift> undefiniert.
Das wars! char wie auch unsigned char wird als int gerechnet, daher
multipliziert der Compiler. Castet man die Operanden nach unsigned int,
klappts auch mit dem swap.
A. K. schrieb:> In C gilt bekanntlich, dass eine Berechnung mindestens in "int" zu> erfolgen hat, oder zumindest das Ergebnis dem entsprechen sollte. Da <<> auf 8 Bits nicht in den 8 Bits bleibt ist - lokal auf diese Operation> bezogen - eine 8-Bit Rechnung nicht zulässig.
da er vorher aber die obere 4bits abschneidet geht es NIE über 8bin
hinaus. Damit kommt bei 8bit das gleiche wie bei 16bit raus. Damit
dürfte er es optimieren.
Übersetzt das mal für einen AVR, der keinen Multiplier hat. Da kommt
genau das raus, was man erwartet. Schätze dass da eine von Johanns
wohlmeinenden Optimierungen einen Fall erwischt hat, in dem sie nach
hinten losgeht. Denn eigentlich ist die Multiplikation um Längen besser
als ein 16-Bit Shift.
A. K. schrieb:> Die Grenzen der Erkenntnis... Er könnte das auf den Wertebereich prüfen,> tut es aber offensichtlich nicht. Die MUL-Version ist zunächst einmal um> Längen besser als eine normale 16-Bit Version. Das mag insgesamt gesehen> wichtiger sein als dieser Fall, wo es etwas teurer ist.
scheinbar ist die Ursache aber mehr das das Array signed ist. Und das
shift ist bei signed nicht mehr definiert.
Peter II schrieb:> scheinbar ist die Ursache aber mehr das das Array signed ist. Und das> shift ist bei signed nicht mehr definiert.
Da ((a - b) & 0x0F) und (((a - b) & 0x0F) << 4) unabhängig von den Typen
von a und b immer positiv sind, spielen Vorzeichen für das Ergebnis der
Rechnung keine Rolle. Aber es kann den Compiler beim Test auf bestimmte
zu optimierende Muster behindern.
Oft ist es ja andersrum: Eine undefinierte Operation behindert den
Compiler weniger als eine definierte. Da sind im Laufe der Entwicklung
von GCC schon manche Kinnladen runtergefallen.
Das Beispiel lässt sich so nicht übersetzen, und Rumraten hat sich als
eine der aufwändigsten und ineffektivsten Werkzeuge zur
Informationsbeschaffung...
Insbersondere wird bei mit[tm] folgender Code erzeugt, d.g. wenn ich so
lange rumrate, bis ich compilierbaren Code erhalte:
Peter II schrieb:> so ganz sicher bin ich mir da nicht, ob das wirklich definiert ist
Das ist hier unwichtig. Wenn eine Operation für bestimmte Werte
undefiniert ist, dann muss der Compiler diese Fälle überhaupt nicht für
das Ergebnis berücksichtigen und darf auch Code erzeugen, bei dem dir
bei solchen Werten der Weihnachtsbaum abbrennt.
A. K. schrieb:> Eine undefinierte Operation behindert den> Compiler weniger als eine definierte.
Und wie schreibe ich es dann, so daß der Compiler macht was ich will,
und das möglichst effektiv?
Andere Aufgabe, wieder ein Shift um 4, der Compiler ist sehr
einfallsreich:
1
7e6:cfefldir28,0xFF;255
2
7e8:dfefldir29,0xFF;255
3
...
4
data=((~i<<4)&0xF0)|i;// Kanal invertiert in high Nibble, normal in low Nibble
5
7f4:9e01movwr18,r28
6
7f6:84e0ldir24,0x04;4
7
7f8:220faddr18,r18
8
7fa:331fadcr19,r19
9
7fc:8a95decr24
10
7fe:e1f7brne.-8;0x7f8
11
800:8c2fmovr24,r28
12
802:8095comr24
13
804:822borr24,r18
Das in eine Schleife zu packen scheint mir jetzt nicht optimal.
i und data sind uint_8.
In ASM sieht das elegant so aus:
Timm T. schrieb:> Und wie schreibe ich es dann, so daß der Compiler macht was ich will,> und das möglichst effektiv?>> data = ((~i << 4) & 0xF0) | i; // Kanal invertiert in high Nibble,
data = ((uint8_t)(~i << 4) & 0xF0) | i;
Und jetzt der Witz, verschiebe ich die Negation um eine Stelle aus der
Klammer raus, wird aus obigem Code mit Schleife:
1
data=(~(i<<4)&0xF0)|i;// Kanal invertiert in high Nibble, normal in low Nibble
2
7f0:8c2fmovr24,r28
3
7f2:8295swapr24
4
7f4:807fandir24,0xF0;240
5
7f6:8095comr24
6
7f8:807fandir24,0xF0;240
7
7fa:8c2borr24,r28
Was bis auf das eine überflüssige andi schon ziemlich der optimierten
ASM Lösung entspricht.
Nur weil ich aus (~i << 4) ein ~(i << 4) gemacht habe?
Muß ich das verstehen?
Johann L. schrieb:> ...du hast überigens ganze 12 Operationen in die eine Zeile gequetscht.
Nun dachte ich gerade ein Vorteil von C wäre, daß ich Funktionen
zusammenfassen kann und nicht jede Operation einzeln abhandeln muß.
Timm T. schrieb:> Muß ich das verstehen?
Letztlich sind etliche Optimierungen grob gesehen eine Art
Mustererkennung. Es gibt allerlei Muster, die im Zwischencode gesucht
werden, und je nach Phase entweder durch anderen Zwischencode ersetzt
werden, oder darauf spezialisierten Code der Zielmaschine erzeugen.
Diese Muster sind so wenig perfekt wie ihre Autoren und scheitern in
manchen Fällen, in denen sie eigentlich auch anwendbar wären. Oder
werden angewandt, obwohl sie zur Verschlechterung führen. Wichtiger ist,
dass man auf der richtigen Seite irrt. Also lieber 2 Takte schlechter
liegt als manchmal falschen Code erzeugt.
Wenn man das genau verstehen will, dann muss man sich auf die Ebene
begeben, in der solcher Code im Compiler dargestellt wird. Und wie darin
Optimierungsmöglichkeiten erkannt werden.
Timm T. schrieb:> Johann L. schrieb:>> ...du hast überigens ganze 12 Operationen in die eine Zeile gequetscht.>> Nun dachte ich gerade ein Vorteil von C wäre, daß ich Funktionen> zusammenfassen kann und nicht jede Operation einzeln abhandeln muß.
Der Vorteil von C ist nicht, anstatt vertikaler Spaghettis (Assembler)
horizontale Spaghettis (ewig lang Ausdrücke) zu nutzen.
Die von mir vorgeschlagene Hilfsfunktion vermeidet zum einen Redundanz,
nämlich das doppelte (x - '0') & 0xf und zum anderen bewirkt sie, dass
das (Zwischen)Ergebnis nur 8-Bit hat. Bei deinen Termen wird immer
wieder implizit auf int erweitert, und was fürhrt dazu, dass schleißlich
mit 16 Bits gerechnet wird. Erst bei der Zuweisung wird wieder auf 8
Bit reduziert. Außerdem ist mit unsigned i.d.R. angenehmer zu rechnen
als mit signed.
Übrigens ist ~(i << 4) was komplett anderes als ~i << 4. Im ersten Fall
wird sign-extended auf 16 bit, geschoben und dann begiert. Im zweiten
Fall wird sign-extended, negiert, und dann geschoben.
Zu bedenken ist auch, dass der Compiler bestimmte Ausdrücke optimiert,
dass er aber — von wenigen Ausnahmen abgesehen — für jede Variable und
für jeden Term Buchführung betreibt, welche Bits gesetzt, welche nicht
gesetzt, welche unbenutzt und welche unter den Tisch fallen können, ohne
einen der anhängigen Ausdrücke zu ändern.
Johann L. schrieb:> Übrigens ist ~(i << 4) was komplett anderes als ~i << 4.
Das ist mir schon klar, aber durch das Begrenzen auf das obere Nibble
kommt es für mich auf das selbe Ergebnis raus.
Johann L. schrieb:> Bei deinen Termen wird immer> wieder implizit auf int erweitert, und was fürhrt dazu, dass schleißlich> mit 16 Bits gerechnet wird. Erst bei der Zuweisung wird wieder auf 8> Bit reduziert.
Ja sorry, ich habe vor einer Woche mit C angefangen und in dem Projekt
jetzt 8k Flash vollgemüllt. Ich bin da noch nicht sooo erfahren drin...
Deswegen stell ich doch die Fragen: Weil sowas eben nicht im Tutorial
steht.
Timm T. schrieb:> Ja sorry, ich habe vor einer Woche mit C angefangen und in dem Projekt> jetzt 8k Flash vollgemüllt. Ich bin da noch nicht sooo erfahren drin...
die Frage ist ob es wirklich ein den paar Zeilen zusätzlich liegt. Da
wird wohl noch ein größere Problem vorhanden sein. Nicht das irgendwo
float dazugekommen ist.
Peter II schrieb:> Da> wird wohl noch ein größere Problem vorhanden sein. Nicht das irgendwo> float dazugekommen ist.
Nicht stänkern! Die float Grundrechenarten brauchen nur etwa 1kB Code.
Timm T. schrieb:> Ja sorry, ich habe vor einer Woche mit C angefangen und in dem Projekt> jetzt 8k Flash vollgemüllt. Ich bin da noch nicht sooo erfahren drin...
Das ist Alles nicht so wild. Sieh Dir den erzeugten Code an, dann kannst
Du im Zweifelsfall erkennen, welche Konstrukte effektiven Code erzeugen.
Ob da jetzt ein mul oder ein swap verwendet wird, wird Dir irgendwann
völlig egal sein.
msx schrieb:> Nicht stänkern! Die float Grundrechenarten brauchen nur etwa 1kB Code.
was immerhin mehr als 10% von 8k sind. Und damit ist das ein recht
großer brocken!
Peter II schrieb:> was immerhin mehr als 10% von 8k sind. Und damit ist das ein recht> großer brocken!
Blödsinn!
Wenn man es braucht, dann braucht man es. Und wenn float nicht reicht,
dann rechnet man eben double. Wo ist das Problem?
msx schrieb:> Peter II schrieb:>> was immerhin mehr als 10% von 8k sind. Und damit ist das ein recht>> großer brocken!>> Blödsinn!
warum ist die Rechnung Blödsinn?
> Wenn man es braucht, dann braucht man es.
richtig, aber wenn man es nicht braucht und es nur aus "Unwissenheit"
reingemacht hat kann man es sparen
> Und wenn float nicht reicht,> dann rechnet man eben double.
double ist identisch zu float bringt als gar nichts.
> Wo ist das Problem?
es braucht Speicher und diese möchte Timm Thaler sparen.
Wenn Du gerne alles in 8-Bit rechnen willst und verwirrt bist, wenn der
Compiler auf 16-Bit erweitert, dann benutze doch einfach die Option
-mint8.
Allerdings kannst Du dann einige der Library-Funktionen nicht mehr
benutzen. Nicht umsonst wird vor -mint8 hier oft gewarnt.
Gruß, Stefan
msx schrieb:>> double ist identisch zu float bringt als gar nichts.>> Na gut, dann müssen wir nicht weiterreden.
aktuell geht es um AVRhttps://gcc.gnu.org/wiki/avr-gcc
und das ist das nun mal so.
Timm T. schrieb:> Allerdings macht der Compiler> mitunter komische Sachen, und ich würde gern verstehen, warum.
Auch wenn das einen gelernten Assemblerprogrammierer natürlich brennend
interessiert, ist es die falsche Frage. Ein Compiler, egal, welcher,
macht immer, WAS du willst, aber er macht es selten so, WIE du willst.
(Wobei die richtige Formulierung des WAS nicht immer trivial einfach
ist).
Lass den Compiler machen, was er will. Ab und zu in den erzeugten
Assembler-Code zu schauen, ist ok, aber jede einzelne Codezeile auf die
Goldwaage zu legen wird dich nicht weiterbringen.
Oliver
Timm T. schrieb:> ich habe vor einer Woche mit C angefangen
Kein Problem. Wenn die ganzen erfahrenen Programmierer, die sich
momentan von Moby am Nasenring durch diverse Beiträge ziehen lassen, dir
stattdessen hier beim C-Einstieg helfen, bist du in 1 Woche ein
ausgewiesener C-Experte.
Oliver S. schrieb:> Lass den Compiler machen, was er will. Ab und zu in den erzeugten> Assembler-Code zu schauen, ist ok, aber jede einzelne Codezeile auf die> Goldwaage zu legen wird dich nicht weiterbringen.
Es geht mir darum, gleich "richtig" C für AVR anzuwenden, so daß zum
Bleistift solche Sachen wie unnötige float-Einbindung nicht vorkommen -
und nein, es werden keine float eingebunden.
Moby A. schrieb im Beitrag #4390723:
> Ist doch nicht tragisch. Denn
...
> ... immer noch fürn Tiny Projekt einen Cortex einspannen ;-)
Du mußt jetzt sehr tapfer sein: Ich hab das vorher in ASM geschrieben,
und ich würde behaupten, ich kann ASM, denn ich mach das 15 Jahre und
auch für größere Projekte.
Der vorherige ASM war auch 5k groß, und inzwischen sind noch einige
Lock-up-Tables und Menustrings - die bekanntlich ordentlich Speicher
fressen - dazugekommen.
So viel schlechter schneidet der C-Compiler gegenüber meinem ASM Code
nicht ab. Er macht nur manchmal Sachen, die ich anders lösen würde.
Und ich möchte bitte hier einen ASM gegen C Krieg. Ich möchte verstehen,
wie C umgesetzt wird, um es möglichst optimal zu programmieren.
Timm T. schrieb:> Es geht mir darum, gleich "richtig" C für AVR anzuwenden, so daß zum> Bleistift solche Sachen wie unnötige float-Einbindung nicht vorkommen -
Nun ja, dein Ausgangsbeitrag liest sich anders, da geht es um
Unterschiede in der Größenordnung einzelner Bytes. Und das ist zwar für
Compilerbauer interessant, zum lernen von C an sich aber völlig unnötig,
und auch der völlig falsche Weg.
Oliver
Timm T. schrieb:> so daß zum Bleistift solche Sachen wie unnötige float-Einbindung nicht> vorkommen
float tritt man sich nicht nur „aus Versehen“ ein, das benutzt man
absichtlich. Es ist eher eine Falle zu vergessen, dass
1
floatq=3/24;
eben nicht 0.125 ergibt sondern 0, weil die Division noch im
Ganzzahlbereich erledigt wird.
Allerdings wirst du als Umsteiger, wenn du einmal drauf gekommen bist,
dass die Gleitkommazahlen weder sofort um sich schießen noch dich zum
sofortigen Kauf eines ATmega2560 nötigen, bei Bedarf schnell merken,
dass sie zuweilen mehr als bequem sein können. ;-) Die Genauigkeit
der 6 … 7 Dezimalstellen genügt an vielen Stellen, und während man
sich bei Festkommarechnungen eben immer um den Dynamikbereich
kümmern muss und darum, dass dessen Grenzen nie versehentlich mal
in der Praxis überschritten werden können, ist der Dynamikbereich von
Gleitkommazahlen in erster Näherung beliebig groß. :) Zwar gibt's für
den AVR keine 64-bit-double (nichtmal 48-bit wie bei Turbo Pascal),
aber die 32-bit-Operationen sind verdammt kompakt implementiert.
Ich möchte meine nicht vorhandene Perücke verwetten, dass sie in der
erreichbaren Codegröße manch eine handgefeilte
Festkomma-32-Bit-Implementierung in den Sack stecken.
Jörg W. schrieb:> Zwar gibt's für> den AVR keine 64-bit-double
Gibt es doch, man muß sie nur nehmen: IAR-Kickstart für AVR bietet
double. Daß AVR-GCC es nicht kann, bedeutet doch nicht, daß es nicht
ginge oder geht.
msx schrieb:> Gibt es doch, man muß sie nur nehmen: IAR-Kickstart für AVR bietet> double.
Wie weit kommt man denn mit einer amputierten Version für gerade mal
4 KiB Code bei 64-Bit-double-Berechnungen?
Ich denke nicht, dass das eine wirklich sinnvolle Option ist. IAR
ist ein guter Compiler, aber die Kickstart-Version nun gerade wegen
64-bit-double zu empfehlen, klingt mir nicht wirklich plausibel (und
die Bezahlversion ist halt schweineteuer).
Jörg W. schrieb:> Wie weit kommt man denn mit einer amputierten Version für gerade mal> 4 KiB Code bei 64-Bit-double-Berechnungen?
Es auszuprobieren kostet kein Geld, und wenn die reinen Berechnungen im
Vordergrund stehen, kommt man damit auch weit - wo auch immer das sein
mag.
Mit Vorurteilen "geht nicht, gibt's nicht" kommt man jedoch nicht einmal
über den Tellerrand.
msx schrieb:> Es auszuprobieren kostet kein Geld, und wenn die reinen Berechnungen im> Vordergrund stehen, kommt man damit auch weit
Und dann, als Math-Koprozessor benutzen, und den Rest auf einer
anderen CPU mit einem anderen Compiler arbeiten lassen?
Ich mein', Daten sammeln und darstellen muss man ja im Allgemeinen
schon noch, die Rechnung ist dann nur das Mittel zum Zweck.
> Es auszuprobieren kostet kein Geld
Zeit ist Geld, außerdem müsste man dafür erst noch ein Windows
ausbuddeln.
Jörg W. schrieb:> Allerdings wirst du als Umsteiger, wenn du einmal drauf gekommen bist,> dass die Gleitkommazahlen weder sofort um sich schießen noch dich zum> sofortigen Kauf eines ATmega2560 nötigen, bei Bedarf schnell merken,> dass sie zuweilen mehr als bequem sein können.
Das mag schon sein, ich hab eine PID Motorregelung mit Zielführung
programmiert, da passte zwar alles in längstens 24 oder 32 Bit und
selbst die Wurzel aus einer 32 Bit Zahl war kein Hexenwerk. Allerdings
wäre es in C und mit Floats vielleicht entspannter gewesen. Vielleicht
aber auch zu langsam.
Oliver S. schrieb:> dein Ausgangsbeitrag liest sich anders, da geht es um> Unterschiede in der Größenordnung einzelner Bytes.
Nunja, mir kommt es eigentlich nicht auf ein paar Bytes mehr oder
weniger an. Mir ist das nur an diesem überschaubaren Beispiel
aufgefallen.
Daß allerdings der Compiler in meiner ADC-Sample Routine, in der 24 ADC
Kanäle über I2C eingelesen und in ein Array geschrieben werden, erstmal
12 Register pushen und am Ende wieder poppen will, scheint mir doch noch
etwas optimierungsbedürftig. In ASM brauch ich dafür grad mal 4 Register
und den Y-Pointer.
Timm T. schrieb:> ich hab eine PID Motorregelung mit Zielführung programmiert,> da passte zwar alles in längstens 24 oder 32 Bit und selbst die> Wurzel aus einer 32 Bit Zahl war kein Hexenwerk. Allerdings> wäre es in C und mit Floats vielleicht entspannter gewesen.> Vielleicht aber auch zu langsam.
Einzelne neuralgische Routinen kannst du auch mit C verwenden sofern sie
dem ABI genügen: http://gcc.gnu.org/wiki/avr-gcc#Register_Layout
Beispiele für 32-Bit Wurzel (Ganzzahl): AVR Arithmetik: Wurzel> Daß allerdings der Compiler in meiner ADC-Sample Routine, in der 24 ADC> Kanäle über I2C eingelesen und in ein Array geschrieben werden, erstmal> 12 Register pushen und am Ende wieder poppen will, scheint mir doch noch> etwas optimierungsbedürftig. In ASM brauch ich dafür grad mal 4 Register> und den Y-Pointer.
Compiler funktionieren anders als Menschen... Wenn es ein
compilierbares Beispiel dazu gibt kann ich eine Stellugnahme dazu
abgeben.
Timm T. schrieb:> und am Ende wieder poppen will, scheint mir doch noch> etwas optimierungsbedürftig. In ASM brauch ich dafür grad mal 4 Register> und den Y-Pointer.
... um nur den markanten Teil zu zitieren ;-)
In ASM wirst Du immer kürzeren Code schreiben können, nur zu welchem
Preis? Nimm eine 16 Bit Variable, oder besser noch ein Array davon, die
Du auf 32 Bit umstellen mußt. In C änderst Du an einer Stelle int16_t
nach int32_t.
In ASM mußt Du alle Stellen ändern, wo diese Variable angesprochen wird.
Viel Spaß bei der Fehlersuche.
Jörg W. schrieb:> Zeit ist Geld
Es ist kein Problem, in C auch ASM-Routinen aufzurufen. Das ist bei
zeitkritischen ISRs manchmal notwendig. ASM kannst Du ja schon ;-)
Wenn der Compiler es zuläßt (oben genannte Kickstart-Version) kann man
sich für die ASM-Routinen auch Register reservieren, die nicht bei jedem
Aufruf gesichert werden müssen. Noch besser: dafür braucht man nicht
einmal Linux zu installieren.