ich bin im Zuge der Optimierung des Codes eines AVR-GCC-Projekts auf ein mir nicht erklärliches Verhalten des Compilers gestoßen, zu welchem ich gerne Euren Rat einholen möchte. Um das Problem einfach beschreiben zu können, habe ich die entsprechende Codepassage so weit wie möglich simplifiziert, in eine eigene Funktion ausgelagert und ein Wegoptimieren durch das Schreiben auf ein SF-Register verhindert:
Wie ersichtlich, soll einfach das High-Byte einer 16-Bit-Variable mit einer 8-Bit-Variable multipliziert werden, was mit einer normalen 8-Bit-Multiplikation möglich sein sollte. Obwohl ich mit der Typenumwandlung „(uint8_t)“ einer 8-Bit-Multiplikation Nachdruck verleihe, produziert der Compiler eine 16-Bit-Multiplikation, behandelt also „(uint8_t)(B16 >> 8)“ weiter als 16-Bit-Variable.
1
0000004e <_Z5Mul16hj>:
2
4e: 27 2f mov r18, r23
3
50: 30 e0 ldi r19, 0x00 ; 0
4
52: 48 2f mov r20, r24
5
54: 42 9f mul r20, r18
6
56: c0 01 movw r24, r0
7
58: 43 9f mul r20, r19
8
5a: 90 0d add r25, r0
9
5c: 11 24 eor r1, r1
10
5e: 9d bd out 0x2d, r25 ; 45
11
60: 8c bd out 0x2c, r24 ; 44
12
62: 08 95 ret
Ändere ich die Berechnung geringfügig, indem zum High-Byte zusätzlich etwas addiert wird, so behandelt der Compiler den Multiplikator als 8-Bit-Variable und produziert eine erwartete 8-Bit-Multiplikation:
Ebenso wird eine 8-Bit-Multiplikation generiert, wenn die 16-Bit-Variable „B16“ weniger als 8 Mal nach rechts geschoben wird, freilich nur mit der Typenumwandlung „(uint8_t)“.
Ich möchte Euch nun gerne Frage, ob Ihr für dieses Verhalten eine Erklärung habt und wie eine schlanke 8-Bit-Multiplikation erreicht werden kann ohne selbst mit Inline-Assembler die Berechnung überschreiben zu müssen.
Das Projekt wurde im Übrigen für einen ATMega8 mit der AVR8-Toolchain-Version 3.4.5.1522 und der GCC-Version 4.8.1 erstellt.
Ich bedanke mich für das Lesen meiner Frage und schon im Voraus für Eure Antworten.
Obwohl ich mit der
Typenumwandlung „(uint8_t)“ einer 8-Bit-Multiplikation Nachdruck
verleihe, produziert der Compiler eine 16-Bit-Multiplikation, behandelt
also „(uint8_t)(B16 >> 8)“ weiter als 16-Bit-Variable.
Das steht nunmal so in den Integer-Promotion-Rules von C. Vor der Multiplikation werden die Operanden, deren Integertypen kleiner ist als ein int, auf int gecastet.
Ansonsten fällt deine Frage, warum der Compiler irgend etwas so macht, wie er es macht, in die große Kategorie „Ist halt so“.
@Kaj G., Niklas G.
Das Umklammern des gecasteten Ausdrucks hatte ich bereits versucht und erzeugt wie von Niklas geschrieben dasselbe Ergebnis.
@Rick
Ändert man den Summanden auf null, erhält man wieder eine 16-Bit-Multiplikation und das gleiche Ergebnis wie bei „Mul16“. Dasselbe Ergebnis erhält man auch bei einer Addition von 256. Eine 8-Bit-Multiplikation wird jedoch auch bei einem Summanden von 257 produziert, der Compiler berücksichtigt scheinbar die Typenumwandlung im Bezug auf die Addition.
@Oliver S.
Ja, das stimmt schon, merkt man auch bei 8-Bit-Vergleiche, die werden auf 16-Bit erweitert. Allerding kann man mit der Typenumwandlung auf 8-Bit einen 8-Bit-Vergleich erzwingen.
1
if((uint8_t)a>1);
Es scheint ja auch im ursprünglichen Beispiel von „Mul8“ bei Multiplikationen zu funktionieren, allerdings eben nur, wenn der Multiplikation eine zusätzliche Berechnung vorausgeht.
Tja, die Kategorie „Ist halt so“ scheint es wirklich gut zu treffen.
Allerding kann man mit der Typenumwandlung auf
8-Bit einen 8-Bit-Vergleich erzwingen.
Nein.
In C findet immer eine Typpromotion nach int statt.
Daraus folgt, dass Berechnungen und Vergleiche immer mit mindestens int-Breite stattfinden.
Dass der AVR-GCC dann trotzdem oft Code in 8-Bit ausgibt liegt daran, dass er beweisen kann, dass das gleiche Ergebnis rauskommt wie bei der Berechnung mit int.
Da das aber eine Optimierung ist, liegt es in der Natur solcher Optimierungen, dass sie nicht immer 100% in allen Fällen greifen.
Du bist hier einfach nur auf eine solche fehlende Optimierung getroffen.
Da bleibt eigentlich nur:
Code umstellen, dass hoffentlich eine Optimierung greift. Das ist oft schwierig, wie wir hier ja nun sehen.
Compilerupdate und hoffen, dass der neue Compiler das nun optimiert.
Das ändert die ABI auf int = 8 Bit breit.
Ist meistens nicht zu empfehlen, wenn du irgendwelche Fremd-Libs (Inklusive libc!) verwendest. Die avr-libc unterstützt das nicht.
Das ist mir klar, evtl. hätte ich es dazu schreiben sollen, dass diese Option erhebliche Nebeneffekte hat. Habe nur grad keinen avr-gcc zur Hand zum testen.
@MaWin O.
Vielen Dank für die Erklärung. Es war mir so nicht bekannt, dass der Compiler nur dann auf eine 8-Bit-Berechnung umstellen kann, wenn die Optimierung das ergibt. Ich war da der Auffassung, dass Operationen immer in jener Breite durchgeführt werden, die von jener Eingangsvariablen mit der höchsten Breite bestimmt wird. Bei Operationen mit Konstanten ohne Längenangabe dann eben mit 16-Bit, wenn die Variablenbreite kleiner ist.
Da dem scheinbar nicht so ist, wird das Beispiel hier dann wohl tatsächlich einfach eine (triviale) nicht gefundene Optimierung.
@Klaus R.
Die Option „-mint8“ Löst das Problem tatsächlich, es wird in beiden Fällen eine 8-Bit-Multiplikation generiert. Wie MaWin O. jedoch schon geschrieben hat, gibt es dann diverse Warnungen und Probleme an anderen Stellen, wenn damit generell mit 8-Bit gerechnet wird.
Gibt es vielleicht auch eine einfache Möglichkeit (eventuell ein Attribut) „mint8“ nur auf einen bestimmten Codebereich oder einzelne Sourcedateien anzuwenden?
Gibt es vielleicht auch eine einfache Möglichkeit (eventuell ein
Attribut) „mint8“ nur auf einen bestimmten Codebereich oder einzelne
Sourcedateien anzuwenden?
Natürlich kannst du einzelne Sources mit und ohne mint8 übersetzen.
Aber da ist Vorsicht geboten, weil sich das ABI ändert und die zusammengelinkten Module von anderen Voraussetzungen was int in gemeinsam genutzten Symbolen und Funktionssignaturen angeht ausgehen.
Außerdem kannst du wie gesagt in einem mit mint8 übersetzen source praktisch kein libc nutzen.
Ich halte das für eine exzellente Idee, wenn man es darauf anlegt in Zukunft extrem schwer zu debuggende Bugs zu erzeugen.
Ja, das klingt im Großen und Ganzen nicht nach einer wirklich praktikablen Lösung. Da ist wohl ein Ausweichen im Zweifel auf Inline-Assembler eher eine Option, wenn auch dergleichen nicht ganz so elegant anmutet.
Ich war da der Auffassung, dass Operationen immer in jener Breite
durchgeführt werden, die von jener Eingangsvariablen mit der höchsten
Breite bestimmt wird.
Das stimmt im Prinzip auch, aber Mindestgröße ist dabei immer int. Das ist quasi wie im Bierzelt mit dem Mindestverzehr. Wenn also ein Operand kleiner ist als int, wird er erst mal auf int "angehoben" (das nennt sich "integer promotion"). Damit ist natürlich auch das Ergebnis einer Operation immer mindestens vom Typ int.
@MaWin O.
Vielen Dank für die Erklärung. Es war mir so nicht bekannt, dass der
Compiler nur dann auf eine 8-Bit-Berechnung umstellen kann, wenn die
Optimierung das ergibt.
Im Prinzip sagt der Standard, dass Berechnungen nie kleiner als mit int durchgeführt werden. Allerdings gibt es auch die "as-if"-Regel, die besagt, dass der Compiler nicht zwingend alles exakt so machen muss, wie angegeben. Er kann Dinge auch weglassen oder anders machen, solange er sicherstellen kann, dass sich das zu beobachtende Verhalten ("observable behavior") dadurch nicht ändert.
Möglicherweise lässt sich der Optimierer des Compilers durch Verwendung
der _fast-Typen erweichen, d.h. statt uint8_t uint_fast8_t verwenden.
Nein, das funktioniert mit Sicherheit nicht. Das ist nur ein Typalias, der zum Zeitpunkt der Optimierung schon lange aufgelöst ist.
Es ist generell überhaupt gar nicht möglich mit irgendeiner bestimmten Codefolge in C eine ganz bestimmte Asm-Codefolge mit Sicherheit zu produzieren. Das ist alles hochgradig compilerabhängig.
Das einzige worauf man sich verlassen kann ist, dass der generierte Asm-Code sich unter den Bedingungen des C-Maschinenmodells (nicht der realen Hardware!) so verhält als würde der geschriebene C-Code direkt ausgeführt werden ("as-if").
Das beinhaltet aber nicht die Ausführungszeit des Codes und das beinhaltet auch sonst keinerlei Hardwareeigenschaften (keine Interrupts, keine Mehrkernsysteme).
Das C-Maschinenmodell ist abstrakt und vollkommen unabhängig von der Zielhardware.
Damit aber im Fall der AVR-Architektur trotzdem noch halbwegs vernünftiger Code herauskommt, gibt es im AVR-Backend noch einen besonderen nachgeschalteten Optimierungsschritt, der Kenntnis über die Zielhardware hat. Verlassen kann man sich darauf aber auch nicht.
Wenn man eine bestimmte Asm-Codefolge braucht, dann muss man sie in Asm schreiben. Da bleibt keine andere Möglichkeit das sicher und stabil (Compilerupdate) zu erreichen.
Nun, wenn ich mich recht irre, hieß es auch mal, daß der gcc eh' nie für 8-Bit-Architekturen entworfen wurde; in Anbetracht dieser Tatsche liefert der schon verdammt guten AVR-Code.
Man müsste vergleichen, wie sich ein explizit für 8-Bit-Architekturen geschriebener Compiler schlägt, also z.B. der IAR für AVR, oder eben der XC8, den Microchip aber nur in kastrierter Ausführung dem Microchip Studio beilegt. Oder der sdcc, aber der hat AVRs nie so recht unterstützt.
Nun, wenn ich mich recht irre, hieß es auch mal, daß der gcc eh' nie für
8-Bit-Architekturen entworfen wurde;
Richtig.
Man müsste vergleichen, wie sich ein explizit für 8-Bit-Architekturen
geschriebener Compiler schlägt, also z.B. der IAR für AVR, oder eben der
XC8, den Microchip aber nur in kastrierter Ausführung dem Microchip
Studio beilegt. Oder der sdcc, aber der hat AVRs nie so recht
unterstützt.
Es gäbe auch noch clang. Dessen daraus generierter Code sieht so aus:
warum möchtest du eine 8Bit Rechnung erzwingen? TCNT1 ist 16Bit breit. Wenn das Ergebnis größer 255 sein sollte und er würde tatsächlich in 8Bit rechnen wäre das Ergebnis verfälscht. Warum der Versuch nach einem Zwang in 8Bit? Außerdem rechnet er schon deswegen 16Bit weil B16 16Bit breit ist.
Ergänzend: außerdem will er ja tatsächlich ein 16Bit-Resultat. Das ist, was sich nach den Regeln der Mathematik (nicht denen der geistig extrem behinderten C-Comiler) aus der Multiplikation zweier 8Bit-Werte ergibt.
Der avr-gcc ist leider ab >4.6.4 in manchen Belangen schlechter geworden. DAs haben wir hier schon öfter diskutiert. Der 13.0.1 ist wieder etwas besser, aber leider nicht in diesem Fall.
Für 4.6.4 ergibt sich die gewünschte Optimierung zu einer reiner 8x8 Multiplikation.
Wusste nicht, daß es davon einen AVR-Port gibt, aber man lernt ja nie
aus.
Es gibt keinen "AVR-Port" in dem Sinne. Das ist nicht wie bei gcc, wo du für jede Zielarchitektur einen eigenen brauchst. Du benötigst nur einen aktuellen clang, dann kann der automatisch alle Zielarchitekturen, inklusive AVR. Allerdings braucht man halt noch die avr-libc dazu.
vielen Dank für die zahlreichen weiteren Antworten, Erklärungen und Informationen.
@Oliver S.
Ich bin ja eigentlich der Meinung, dass man für ein Projekt irgendwie den falschen Mikrocontroller gewählt hat, wenn man mit Assembler versuchen muss, einzelne Zyklen zu optimieren. Es ist bei mir jedoch schon vorgekommen, dass sich im Laufe des Projekts die (selbst gesteckten) Anforderungen ändern und irgendwann stellt man fest, dass die Rechenleistung nicht mehr so recht ausreicht. Weil man aber schon viel Zeit investiert hat, möchte man auch nicht unbedingt alles wieder ändern, weil die Rechenleistung ja „nur ganz knapp“ nicht reicht. Das ist dann so ein Punkt, wo man sich gerne einen effizienten Code wünscht und am Ende Teile in Assembler integrieren muss.
Bei diesem Projekt geht es um die Berechnung der Mischfarbe von Pixeln. Im Prinzip ist es nicht so kritisch wie lange die Berechnung dauert, da die Framerate sehr niedrig ist. Da jedoch in Summe sehr viele Pixel zu berechnen sind, habe ich mir den entsprechenden erzeugten Maschinencode angesehen und eben festgestellt, dass er nicht ganz so effizient ist.
@Rolf M., MaWin O.
Danke für die weitere Erklärung der Compilertechniken. Legt der C-Standard dann auch die tatsächliche Breite der Größe „int“ fest? Soweit ich das einmal gelernt habe, kann die Breite von „int“ anhängig vom System variieren. Auf PC-Ebene – zumindest in Visual Studio – ist ein „int“ 32-Bit breit. Eine Bitbreite von 16-Bit für den Typ „int“ scheint auch weit verbreitet zu sein. Wenn der C-Standard nun vorschreibt, dass die Mindestgröße vom Typ „int“ sein muss, wird ja eigentlich nicht die konkrete Bitbreite vorgeschrieben. Oder legt der C-Standard auch fest, dass „int“ auch mindestens 16-Bit breit sein muss?
Interessantes Ergebnis des clang-Compilers. Immerhin merkt der Compiler dass nur eine 8-Bit-Multiplikation nötig ist, verspielt es jedoch wieder mit den Null-Veroderungen.
@Harald K.
Danke für diese Idee, wie MaWin O. schon prophezeit hat, erzeugt auch der Fast-Typ denselben 16-Bit-Multiplikationscode.
@Veit D., c-hater
Wie c-hater schon geschrieben hat, dient die Zuweisung an „TCNT1“ lediglich dem Verhinder des Optimierens. Die eigentliche Berechnung (Mischfarbenberechnung) ist umfangreicher, weshalb ich das Problem auf das Nötigste verkürzt habe.
@Wilhelm M.
Das ist eine interessante Erkenntnis, dass die ältere Compilerversion diese Optimierung findet. Sind dir auch Fälle bekannt, wo der ältere Compiler ineffizienteres Ergebnis liefert als eine neuere Version?
Legt der
C-Standard dann auch die tatsächliche Breite der Größe „int“ fest?
Nicht vollständig. Es sind relative Beziehungen zwischen den Typen festgelegt.
Soweit ich das einmal gelernt habe, kann die Breite von „int“ anhängig
vom System variieren.
Richtig.
Auf PC-Ebene – zumindest in Visual Studio – ist
ein „int“ 32-Bit breit.
Das hat erst einmal nichts mit "PC" zu tun.
Das wird in der ABI definiert. (Application Binary Interface).
Die ist grob gesagt systemabhängig, aber ein System kann gleichzeitig mehrere ABIs unterstützen.
Linux kann z.B. gleichzeitig die x64, i386 und x32 ABIs auf der PC Plattform unterstützen.
Oder legt der
C-Standard auch fest, dass „int“ auch mindestens 16-Bit breit sein muss?
Glaube schon. Bin mir aber nicht mehr ganz sicher.
Ist aber bestimmt nachzulesen in diesem Internet. :)
@Rolf M., MaWin O.
Danke für die weitere Erklärung der Compilertechniken. Legt der
C-Standard dann auch die tatsächliche Breite der Größe „int“ fest?
Er legt fest, dass int mindestens den Bereich -32767 bis +32767 unterstützen muss, was sich mit weniger als 16 Bit natürlich nicht erreichen lässt. Außerdem muss seine Größe ein ganzzahliges Vielfaches der Größe von char sein. Und dann gilt noch sizeof(char)<=sizeof(short)<=sizeof(int)<=sizeof(long)<=sizeof(long long).
Ein Compiler dürfte also laut C-Standard theoretisch also auch so aufgebaut sein, dass char, short, int, long und long long alle die gleiche Größe von z.B. 71 Bit haben. Allerdings wäre das eine eher ungewöhnliche Wahl 😀.
Soweit ich das einmal gelernt habe, kann die Breite von „int“ anhängig
vom System variieren.
Das ist richtig. Im Prinzip war es so gedacht, dass int nach Möglichkeit die native Größe der jeweiligen CPU haben sollte (was aber vom Standard nicht vorgeschrieben ist). Daher ist bei 16-Bit-Architekturen int 16 Bit breit, bei 32-Bit-Architekturen entsprechend 32 Bit. Für 8-Bitter geht es aber nicht, auf Grund der Mindestanforderung von int. Bei 64-Bittern ist int in der Regel (bis auf ein paar Ausnahmen) immer noch 32 und nicht 64 Bit, einfach weil einem sonst die Datentypen nach unten hin ausgehen. Denn in C dürfen ja wie oben geschrieben nur char und short kleiner sein als int, aber man möchte ja auch Datentypen für 8, 16, und 32 Bit unterstützen, bräuchte also drei Typen, die kleiner sind.
int wird im ANSI C mit Mindestgröße von 2 Byte festgelegt, was 16 Bit signed sind. Minimum! Auf einem 8 Bit AVR sind int 16 Bit signed. Auf deinem PC werden es sicherlich 32bit signed sein. Im Zweifel mit sizeof() Datentypgröße anzeigen lassen. Möchtest du konkrete Datentypengrößen verwenden, dann nimm int16_t oder int32_t usw., dann sagt dessen Name was Programm ist.
korrekt. War mehr gedacht das er sich überlegen kann ob er int, long oder int16_t, int32_t schreibt und weniger rätseln muss. Nur für den Fall der Fälle.
@Oliver S., MaWin O., Rolf M., Veit D.
Danke für die Erläuterung. Das erklärt dann natürlich, warum sich für 8-Bit-Systeme die Breite von „int“ nicht an die Systembreite anpassen und nur die Optimierung hier zu schlankerem Maschinencode führen kann.
@Jörg W.
Wie im Eingangsbeitrag am Ende angeführt habe ich das Projekt für einen ATMega8 mit der AVR8-Toolchain-Version 3.4.5.1522 und der GCC-Version 4.8.1 erstellt. Beides mitgeliefert mit der IDE „Atmel Studio 6“ in Version 6.2.1502 SP2.
Das ist wohl eine nicht mehr ganz taufrische IDE dessen Editor auch teilweise mühsam ist, aber es läuft nun einmal soweit, man kennt das ja vielleicht.
Was ich bisher vergessen habe zu erwähnen – wenn es denn relevant ist – das Beispiel wurde als C++-Projekt angelegt und als Release mit der Optimierungsstufe „Os“ übersetzt.
@Veit D.
Ich verwende bei Code für Mikrocontroller eigentlich ausschließlich Integertypen mit festgelegter Breiter in der benötigen Größe in der Hoffnung, so die beschränkte Rechenleistung Effizient nutzen zu können. Dennoch Danke für den Ratschlag.
Wobei das nicht von der Integer-Promotion entbindet
Da gilt aber immer noch die "as if"-Regel. Da die zweite Multiplikation mit 0 multipliziert, könnte der Compiler das durchaus wegoptimieren, ohne die Promotion-Regeln zu verletzen.
Was ich bisher vergessen habe zu erwähnen – wenn es denn relevant ist –
das Beispiel wurde als C++-Projekt angelegt und als Release mit der
Optimierungsstufe „Os“ übersetzt.
Prima.
Wenn es C++ ist, dann kannst Du es so schreiben:
1
#include<avr/io.h>
2
#include<stdint.h>
3
4
uint16_tb;
5
uint8_ta;
6
7
volatileuint8_tr;
8
9
template<typenameA,typenameB>
10
AMul(constAa,constBb){
11
constexpruint8_tshift=(sizeof(B)-sizeof(A))*8;
12
returnstatic_cast<A>(b>>shift)*a;
13
}
14
15
intmain(){
16
r=Mul(a,b);
17
returnr;
18
}
oder
1
r=Mul<uint8_t>(a,b);
und bekommst das (fast) optimale Resultat:
1
ldsr25,b+1
2
ldsr24,a
3
mulr24,r25
4
movr24,r0
5
clrr1
6
stsr,r24
7
ldsr24,r
8
ldir25,0
Dies ist etwas, was ich manchmal beobachte: wenn man generischen Code schreibt, produziert der gcc besseren Code.
Im Beispiel von Johann wird der Shift als MULS implementiert, was einen
Zyklus länger dauert als die Variante mit drei LSL und ein zusätzliches
Register benötigt. Die Codegröße ist in beiden Fällen gleich. Beide
Varianten enthalten einen überflüssigen Befehl (CLR bzw. MOVW) und sind
deswegen sowieso nicht perfekt. Die Differenz von einem Taktzyklus
reicht offensichtlich nicht aus, das Problem als dringlich einzustufen.
Im Beispiel hier im Thread wird eine 16x16->16-Multiplikation nicht als
8x8->16-, sondern nur als 16x8->16-Multiplikation optimiert. Der Shift
(hier um 8 Bits) wird – anders als bei Johann – gar nicht explizit
ausgeführt. Deswegen haben die beiden Probleme nur wenig miteinander zu
tun.
Das Problem hier im Thread würde ich als schwerwiegender ansehen, da
hier die unvollständige Optimierung nicht nur mehr Zyklen (4), sondern
auch mehr Flash-Bytes (8) kostet,
Von daher gesehen hätte ein neuer Report zu diesem Problem vielleicht
eine höhere Chance auf Berücksichtigung.
@Wilhelm:
Falls du das Problem noch einmal einstellen möchtest, solltest du das
Template weglassen. Codebeispiele in PRs sollten sich auf das
Wesentliche konzentrieren, und da das Template nicht die Ursache des
Problems ist, verschleiert es das eigentlich Problem nur unnötig.
Falls du das Problem noch einmal einstellen möchtest, solltest du das
Template weglassen. Codebeispiele in PRs sollten sich auf das
Wesentliche konzentrieren, und da das Template nicht die Ursache des
Problems ist, verschleiert es das eigentlich Problem nur unnötig.
Das stimmt. Werde das korrigieren bzw. ein neuen PR aufmachen.
@Jörg W.
Stimmt, ich bin richtig erstaunt, welche hohe Versionsnummer der neueste Compiler hat. Ich sollte wohl die gesamte Programmiersoftware erneuern und gleich auf einen Open Source Editor umsteigen. Das Konfigurieren schreckt mich da jedoch ein wenig ab.
@Wilhelm M.
Vielen Dank für Dein Engagement in dieser Sache und dem Erstellen eines Fehlerreports.
Mich wundert es ja ein wenig, dass die fehlende Optimierung über sehr viele Versionen hinweg noch keinem Aufgefallen ist oder zumindest gestört hat, da eine derartige Berechnung meines Erachtens nicht so Exotisch erscheint.
Gibt es eigentlich in der von der AVR8-Toolchain verwendeten Bibliotheken eine Funktion, mit der sich das High-Byte extrahieren lässt, oder wäre das dann ohnedies nur ein einfaches Shift-Makro?
@Wilhelm M.
Vielen Dank für Dein Engagement in dieser Sache und dem Erstellen eines
Fehlerreports.
Mich wundert es ja ein wenig, dass die fehlende Optimierung über sehr
viele Versionen hinweg noch keinem Aufgefallen ist oder zumindest
gestört hat, da eine derartige Berechnung meines Erachtens nicht so
Exotisch erscheint.
Es war schon längst aufgefallen, ich war nur zu blöd zum Suchen:
Gibt es eigentlich in der von der AVR8-Toolchain verwendeten
Bibliotheken eine Funktion, mit der sich das High-Byte extrahieren
lässt, oder wäre das dann ohnedies nur ein einfaches Shift-Makro?
Aus C-Sicht wäre das schon nur Shift oder Division durch 256.
Gibt es eigentlich in der von der AVR8-Toolchain verwendeten
Bibliotheken eine Funktion, mit der sich das High-Byte extrahieren
lässt, oder wäre das dann ohnedies nur ein einfaches Shift-Makro?
Mich wundert es ja ein wenig, dass die fehlende Optimierung über sehr
viele Versionen hinweg noch keinem Aufgefallen ist oder zumindest
gestört hat, da eine derartige Berechnung meines Erachtens nicht so
Exotisch erscheint.
Es gibt recht viele Fälle, wo der GCC Code mit 16-Bit-Operationen
erzeugt, wo auch 8-Bit-Operationen genügten. Das hängt u.a. damit
zusammen, dass der GCC ursprünglich für die auf unixoiden System
eingesetzten Prozessoren, also nicht für 8-Bit-Prozessoren vorgesehen
war.
Die Optimierung für 8-Bit-Prozessoren erfordert deswegen sehr viel
spezifische Detailarbeit, um alle denkbaren Fälle abzudecken. Johann hat
da schon viel Arbeit hineingesteckt mit deutlich sichtbarem Erfolg.
Trotzdem gibt es immer noch Kombinationen von Operationen, wo seine
Optimierungen nicht greifen.
Solange die Beschränkungen in der Optimierung nur wenige Taktzyklen oder
Programmbytes kosten, werden entsprechende PRs wohl als wenig dringlich
eingestuft oder gar nicht erst erstellt.
Gibt es vielleicht auch eine einfache Möglichkeit (eventuell ein
Attribut) „mint8“ nur auf einen bestimmten Codebereich oder einzelne
Sourcedateien anzuwenden?
Ich wüßte nicht, wie... aber stelle mir auch die Frage, ob es wirklich nötig ist, solche Mikrooptimierungen zu betreiben. Damit sich so etwas lohnt, muß das Timing schon extrem kritisch und der betreffende Code vergleichsweise oft aufgerufen werden, und in solchen Fällen fährt man sicherlich mit Inline-Assembler besser. In allen anderen Fällen gelten die Leitsätze von Donald E. Knuth ("premature optimization is the root of all evil"), Kirk Pepperdine ("measure, don't guess"), und Kent Beck ("make it work, make it right, make it fast"). Oder, anders gesagt: in den meisten Fällen sind solche Mikrooptimierungen kontraproduktiv, weil sie die Les- und Wartbarkeit Deines Code verschlechtern, und nutzlos, weil es in einem fertigen und vom Compiler optimierten Programm meist ganz andere Stellen gibt, an denen eine Optimierung wesentlich lohnenswerter ist.
Nein, denn letztlich ändert -mint8 das ABI. Wie schon geschrieben wurde, bist du damit nicht nur inkompatibel zum C-Standard, sondern auch zur avr-libc.
@Jörg W., Wilhelm M.
Ja, das ist schon klar, dass achtmal Rechtsschieben oder eine Division durch 256 rechnerisch das High-Byte einer 16-Bit-Variale extrahiert. Auf 8-Bit-Maschinenebene wäre das dann logischer weise einfach das direkte Auswählen des High-Bytes eines Registerpaares für weitere Berechnungen. Ich dachte mir nur, es gibt vielleicht eine Inline-Assembler-Funktion, die von der übergebenen Variable das höherwertige Register (wohl ein ungerades Register) auf ein niederwertiges Rückgaberegister (wohl das gerade Register 24) kopiert. Somit würde der Multiplikator dann schon in einem geraden Register stehen, der Compiler dies als 8-Bit Multiplikator erkennen und auf die, von den Integer-Promotion-Regeln vorgegebene, Erweiterung auf 16-Bit verzichten.
Also klar, das macht so natürlich keinen Sinn, da der Compiler ja einfach selbst zur weiteren Berechnung das obere Register des Registerpaares direkt wählen kann. Es war im Prinzip nur eine Überlegung der Optimierung etwas nachzuhelfen, da kleine Änderungen (wie eben das zusätzliche Addieren einer Konstanten größer null) scheinbar die Berechnungslogik so ändern, dass die Optimierung bei der Multiplikation greift.
Aber ja, würde der Funktionsaufruf tatsächlich durchgeführt werden und nicht als Inline ausgeführt werden, wäre die Zyklusersparnis nicht mehr gegeben. Auch würden wohl unnötig viele Kopieroperationen hinzukommen wodurch die Effizienzsteigerung kaum vorhanden wäre.
@Yalu X.
Ja, ich kann mir schon vorstellen, dass es nicht sehr einfach ist einen Optimierungsfall zu erkennen. Oft wird zwar die Optimierung dieselbe sein, aber die Ausgangssituation eine andere was wohl die Erkennung erschwert.
@Sheeva P.
Das stimmt wohl. Die Lesbarkeit leidet unter so mancher Konstruktion in der Hoffnung auf Optimierung schon sehr deutlich. Auch habe ich schon festgestellt, dass die Kompaktheit des erzeugten Codes nicht wirklich mit einer ausgeklügelten Optimierung der C-Formulierung steigt. Beispielsweise diverse Schiebeoperation um möglichst effizient Dividieren zu können werden vom Compiler meist auch selbst gefunden.
Im Zuge des Verfassens dieser Antwort habe ich mich dran Erinnert, dass gelegentlich das Konstrukt der Union für das Auslesen einzelner Bytes eines größeren Datentyps zweckentfremdet wird und mir die Frage gestellt, ob das in diesem Fall den Compiler dazu bringen würde, die Multiplikation mit 8-Bit durchzuführen:
Zu meiner Überraschung generiert der Compiler die erwartete Effiziente 8-Bit-Multiplikation:
1
0000004e <_Z5Mul16hj>:
2
4e: 78 9f mul r23, r24
3
50: c0 01 movw r24, r0
4
52: 11 24 eor r1, r1
5
54: 9d bd out 0x2d, r25 ; 45
6
56: 8c bd out 0x2c, r24 ; 44
7
58: 08 95 ret
Soweit ich das in Erinnerung habe, ist das Zweckentfremden einer Union für das Manipulieren von Bytes von Variablen keine sonderlich gute Idee, da wohl die Zuordnung der Speicherbereiche nicht garantiert werden kann. In diesem Beispiel scheinen sich in der Union „U“ „I16“ und „A8[2]“ jedoch ein Registerpaar (R22/R23) zu teilen und mit „U.A8[1]“ der Zugriff auf das High-Byte von „U.I16“ respektive „I16“ der Eingangsvariable möglich. Durch die direkte Übergabe einer nun 8-Bit-Variable scheint die Optimierung des Compilers eine 8-Bit-Multipliktion für ausreichend zu erachten.
Im Zuge des Verfassens dieser Antwort habe ich mich dran Erinnert, dass
gelegentlich das Konstrukt der Union für das Auslesen einzelner Bytes
eines größeren Datentyps zweckentfremdet wird und mir die Frage
gestellt, ob das in diesem Fall den Compiler dazu bringen würde, die
Multiplikation mit 8-Bit durchzuführen:
Das ist in C++ leider UB (in C wäre es legal). Du liest vom nicht-aktiven Element der union.
Bevor nun alle in Entrüstung fallen: ja, ich weiß, dass der avr-g++ dies wie gewünscht übersetzt ;-)
@Jörg W., Wilhelm M.
Ja, das ist schon klar, dass achtmal Rechtsschieben oder eine Division
durch 256 rechnerisch das High-Byte einer 16-Bit-Variale extrahiert. Auf
8-Bit-Maschinenebene wäre das dann logischer weise einfach das direkte
Auswählen des High-Bytes eines Registerpaares für weitere Berechnungen.
Im Zuge des Verfassens dieser Antwort habe ich mich dran Erinnert, dass
gelegentlich das Konstrukt der Union für das Auslesen einzelner Bytes
eines größeren Datentyps zweckentfremdet wird und mir die Frage
gestellt, ob das in diesem Fall den Compiler dazu bringen würde, die
Multiplikation mit 8-Bit durchzuführen
Man kann aber Deine Idee aufgreifen und folgendes schreiben:
Ich habe gerade den avr-gcc 13.0.1 mit einem Patch versorgt, und nun produziert er nun wieder das optimale Ergebnis, wie auch schon vor langer Zeit avr-gcc 4.6.4.
@Wilhelm M.
Vielen Dank für Deinen weiteren Einsatz sowie der Beispiele für einen C++-Konformen Zugriff auf einzelne Bytes eines Datentyps . Wie ich gesehen habe, hast Du in einem Beitrag „std:bit_cast“ hier vorgestellt. In der auf meinem System installierten Compilerversion ist dies zwar nicht verfügbar, aber der Alternative Umweg über „memcpy“ funktioniert.
In Anlehnung an diese Methode habe ich versucht, „memcpy“ in einfacher Weise direkt mit wildem Pointer-Casting nachzuahmen:
Der Compiler erzeugt zwar damit auch eine einfache 8-Bit-Multiplikation, allerdings ist der zusätzliche Overhead besonders umfangreich. Im Gegensatz zu „memcpy“ wird das Array nicht in Registern gehalten, sondern in das RAM ausgelagert, was natürlich enorm viel Effizienz kostet:
1
0000004e <_Z5Mul16hj>:
2
4e: cf 93 push r28
3
50: df 93 push r29
4
52: 00 d0 rcall .+0 ; 0x54 <_Z5Mul16hj+0x6>
5
54: cd b7 in r28, 0x3d ; 61
6
56: de b7 in r29, 0x3e ; 62
7
58: 7a 83 std Y+2, r23 ; 0x02
8
5a: 69 83 std Y+1, r22 ; 0x01
9
5c: 9a 81 ldd r25, Y+2 ; 0x02
10
5e: 98 9f mul r25, r24
11
60: c0 01 movw r24, r0
12
62: 11 24 eor r1, r1
13
64: 9d bd out 0x2d, r25 ; 45
14
66: 8c bd out 0x2c, r24 ; 44
15
68: 0f 90 pop r0
16
6a: 0f 90 pop r0
17
6c: df 91 pop r29
18
6e: cf 91 pop r28
19
70: 08 95 ret
Erstaunlich finde ich im Übrigen die beiden Befehle „pop r0“, wo doch „push r0“ zuvor nie ausgeführt wird, sowieso den mir nicht schlüssigen „rcall“-Befehl.
Interessant, was sich im Beitrag Deines Bug-Reports tut, danke für das Erstellen desselbigen. Hier scheint es doch ein Interesse zu geben und womöglich wird der Patch in der nächsten Version integriert.
Erstaunlich finde ich im Übrigen die beiden Befehle „pop r0“, wo doch
„push r0“ zuvor nie ausgeführt wird, sowieso den mir nicht schlüssigen
„rcall“-Befehl.
rcall .0 schafft einen 2-Byte großen Stackframe, der dann im weiteren benutzt wird. Die pops am Ende räumen den wieder ab.
Ah, okay, also ein Funktionsaufruf an dieselbe Programmstelle, womit die im Stack gespeicherte Rücksprungadresse als Datenspeicher zweckentfremdet werden kann. Weil die Rücksprungadresse also geändert wird, kann nicht mit „ret“ der Aufrufbefehl abgeschlossen werden, sondern es wird mit „pop r0“ beendet.
Danke für die Erklärung. Ja, das ist wahrlich gcc-Assembler-Magie, sehr gefinkelt ;-).
In Anlehnung an diese Methode habe ich versucht, „memcpy“ in einfacher
Weise direkt mit wildem Pointer-Casting nachzuahmen:
In diesem Fall hast Du Glück und es kein UB: Du machst einen pointer-cast und dereferenzierst. Dies ist normalerweise UB, sofern der Zieltyp nicht (unsigned) char ist. Dies ist bei Dir der Fall (uint8_t) und deswegen kein UB.
Heute macht man das aber per std::bit_cast. Dann ist es sogar constexpr und auf jedenfall expressiver. Die Notlösung ist std::memcpy, was ja gar kein Copy macht wie Du siehst, weil hier std::memcpy ein Spezialfall ist, der von allen Compilern entsprechend behandelt wird.
Man sollte sich bei solchen Aktionen aber erstmal fragen, ob man nicht versehentlich Mikro-Optimierung betreibt.
D.h. bringt die Optimierung an dieser Stelle wirklich den Meilenstein in viel schnellerer Ausführung des Gesamtprogramms bzw. besonders zeitkritischer Codeabschnitte oder eine bedeutende Senkung des Flashverbrauchs.
@Wilhelm M.
Ein Pointer-Cast von „long“ auf „int“ wäre dann ein undefiniertes Verhalten, obwohl ein „int“ weniger oder zumindest gleichviele Bytes groß ist als der Datentyp „long“? Wird dann sozusagen nur garantiert, dass an der gecasteten Adresse zumindest ein Byte Gültigkeit hat?
@Peter D.
Zweifelsohne ist das so. Wie schon geschrieben, spielt es in meinem Fall keine allzu große Rolle. Ich habe mir eben jenen Teil des kompilierten Programms angesehen, der in einer Schleife sehr oft aufgerufen wird (Pixelberechnung) um zu sehen ob ich das Optimieren kann und festgestellt, dass nicht nötiger Weise mit 16-Bit multipliziert wird. Nachdem ich einen Fehler im Programmcode ausgeschlossen habe, wollte ich einfach wissen wie es dazu kommt, was mir hier ja ausführlich erklärt wurde.
Dank des Erstellens eines Fehlerberichts von Wilhelm wurde ja sogar eine Optimierung des Optimierers angestoßen. Wenn dies in künftige Compilerversionen einfließt ist das doch eine kleine Verbesserung.
Man sollte sich bei solchen Aktionen aber erstmal fragen, ob man nicht
versehentlich Mikro-Optimierung betreibt.
Natürlich sollte man in der oben geschilderten Art seinen Quelltext nicht modifizieren, das wäre ja Quatsch. Das waren auch nur Testcases, um ggf. dem missed-optimization-bug auf die Spur zu kommen.
D.h. bringt die Optimierung an dieser Stelle wirklich den Meilenstein in
viel schnellerer Ausführung des Gesamtprogramms bzw. besonders
zeitkritischer Codeabschnitte oder eine bedeutende Senkung des
Flashverbrauchs.
Der endgültige Patch für den avr-gcc, den ich jetzt teste, produziert in Code mit einigen solcher 8x8 Multiplikationen ca. 10% kleineren Code und natürlich auch schnelleren Code (wie stark sich das auswirkt, habe ich noch nicht vermessen, doch das kannst Du Dir auch selbst ausrechnen anhand des besseren Assembler).
Daher halte ich das nicht für eine Mikro-Optimierung. Ob und wann die jetzt mainline geht, weiß ich nicht. Ist mir aber auch egal, da ich meinen avr-gcc eh selbst erzeuge.
@Wilhelm M.
Ein Pointer-Cast von „long“ auf „int“ wäre dann ein undefiniertes
Verhalten, obwohl ein „int“ weniger oder zumindest gleichviele Bytes
groß ist als der Datentyp „long“? Wird dann sozusagen nur garantiert,
dass an der gecasteten Adresse zumindest ein Byte Gültigkeit hat?
Das spielt gar keine Rolle, weil es einfach formal UB ist, wenn der Zieltyp nicht char ist. Auch wenn es in der Praxis funktionieren wird (principle of least surprise).
Wie oben schon gezeigt, ist std::memcpy() bzw. std::bit_cast die einzige Möglichkeit, ein type-punning korrekt auszuführen. Allerdings gibt da prinzipiell natürlich auch die Möglichkeit von trap-representations, also nicht gültigen Bit-Kombinationen im Zieltyp. Kann man dies aber ausschließen im konkreten Fall, so liegt kein UB mehr vor.
Dank des Erstellens eines Fehlerberichts von Wilhelm wurde ja sogar eine
Optimierung des Optimierers angestoßen. Wenn dies in künftige
Compilerversionen einfließt ist das doch eine kleine Verbesserung.
Aktuell testen wir noch einen Patch. Dieser zeigt aktuell das richtige Optimierungsverhalten und läuft auch in kompilizierten Fällen ohne ICE. Die Codegrößer schrumpft dabei um ca. 10% bei Code, der viel solche Sachen enthält. Von daher bestehen gute Chancen auf Integration in mainline, und damit ist der avr-gcc > 13.0.1 dann wieder so gut wie 4.6.4. Na, wenn das mal nicht gut ist ;-)
Ein Pointer-Cast von „long“ auf „int“ wäre dann ein undefiniertes
Verhalten, obwohl ein „int“ weniger oder zumindest gleichviele Bytes
groß ist als der Datentyp „long“? Wird dann sozusagen nur garantiert,
dass an der gecasteten Adresse zumindest ein Byte Gültigkeit hat?
Es ist verboten auf eine Variable über einem falschen (zum Variablentyp unpassenden) Pointertyp zuzugreifen (Type Punning). Eine Ausnahme gibt es nur für char.
Das heißt, wenn man long-pointer -> int-pointer castet, dann muss man wieder zurück auf long-pointer casten bevor man den Zeiger dereferenziert.
Ansonsten erzeugt man UB.
Man kann auch einen zeitkritischen Codeteil nach Assembler compilieren, das *.S selber optimieren und alles zusammen linken.
Das finde ich deutlich lesbarer als inline Assembler.
Aufpassen, daß man das Compilieren nach Assembler wieder aus dem Make rausnimmt, sonst wird das Optimierte wieder überschrieben.
Man kann auch einen zeitkritischen Codeteil nach Assembler compilieren,
das *.S selber optimieren und alles zusammen linken.
Das finde ich deutlich lesbarer als inline Assembler.
Aufpassen, daß man das Compilieren nach Assembler wieder aus dem Make
rausnimmt, sonst wird das Optimierte wieder überschrieben.
Dafür programmiere ich aber nicht in C / C++. Optimierung ist eine der vornehmsten Aufgaben eines Compilers. Und diese hier angesprochene Optimierung ist doch wohl sehr erwartbar. Sonst reden doch hier auch alle davon, wie toll die Compiler dies und das optimieren und das man Kraut- und Rübencode schreiben können, der Compiler wird's schon richten. Und dann bei so einem simplen Fall auf Assembler gehen: das ist doch wohl nicht Dein Ernst.
Aufpassen, daß man das Compilieren nach Assembler wieder aus dem Make
rausnimmt, sonst wird das Optimierte wieder überschrieben.
Das kann eigentlich nur passieren, wenn man ein lbödes OS hat, das nicht zwischen *.s und *.S unterscheiden kann. GCC erzeugt von sich aus (etwa mit -S oder zusammen mit -save-temps) nämlich nur *.s, aber nie *.S.
Abhilfe schafft dann, Endung *.sx zu verwenden. Oder Endung wie *.asm nach gusto, aber dann mit -x assembler-with-cpp vor dem Modul.
Das kann eigentlich nur passieren, wenn man ein lbödes OS hat, das nicht
zwischen *.s und *.S unterscheiden kann.
LOL
Es ist viel aufwendiger und komplizierter, eine "case-independend"-Benamsung korrrekt umzusetzen. Jeder, der was vom Programmieren versteht, kann mir da nur zustimmen.
Microsoft hat es geschafft (so ungefähr ab W2k dann endlich sogar wirklich fehlerfrei).
Nur die wirklich blöden OS' können's bis heute nicht. Hängen also entwicklungsmäßig in dieser Sache ungefähr ein Vierteljahrhundert hinterher. Und das Schlimme ist: es ist keine Besserung absehbar, weil es dann an allen Ecken und Enden heftigst knirschen würde im Gebälk...
Microsoft hat es geschafft (so ungefähr ab W2k dann endlich sogar
wirklich fehlerfrei).
Nein. Nicht wirklich. Das kann man gar nicht fehlerfrei umsetzen, weil "Case" gar nicht global eindeutig definiert ist, außer vielleicht für die Buchstaben a-z. Schon gar nicht zu der Zeit, wo MS mit dem Unsinn angefangen hat.
Das ist ein absolut sinnloses Feature, was man gar nicht korrekt implementieren kann.
Man kann es höchstens für die eigene Definition von "Case" korrekt implementieren.
Microsoft hat es geschafft (so ungefähr ab W2k dann endlich sogar
wirklich fehlerfrei).
Auch Windows 10 und vermutlich Windows 11 können sich immer noch nicht
entscheiden, ob sie Dateinamen case-sensitiv oder case-insensitiv
behandeln sollen, wie folgende Beispiele zeigen:
Ich habe gerade in ein und demselben Verzeichnis erfolgreich drei
verschiedene Dateien angelegt, deren Namen sich nur in ihrer
Groß-/Kleinschreibung unterscheiden:
straße.txt
STRASSE.TXT
STRAẞE.TXT
Dann sollten doch erst recht die beiden folgenden Dateinamen, die sich
nicht nur in der Groß-/Kleinschreibung, sondern auch in der Bedeutung
ganz klar unterscheiden, parallel existieren können:
Ich habe gerade in ein und demselben Verzeichnis erfolgreich drei
verschiedene Dateien angelegt, deren Namen sich nur in ihrer
Groß-/Kleinschreibung unterscheiden:
straße.txt
STRASSE.TXT
STRAẞE.TXT
Nee, der mittlere fällt raus. Daß MS das Versal-SZ nicht korrekt behandelt, /das/ ist in der Tat ein Fehler, aber der Helvetizismus "Strasse" ist nicht das gleiche wie "Straße".
Nee, der mittlere fällt raus. Daß MS das Versal-SZ nicht korrekt
behandelt, das ist in der Tat ein Fehler, aber der Helvetizismus
"Strasse" ist nicht das gleiche wie "Straße".
STRASSE ist kein Helvetizismus, sondern die gängige Art in Deutschland, das Wort "Straße" zu versalieren. So steht's auch im amtlichen Regelwerk zur Rechtschreibung, wobei alternativ auch das ẞ erlaubt ist.
Das ẞ als "großes" ß hat sich bisher nicht wirklich durchsetzen können. Es passt vom Schriftbild her auch nicht sonderlich gut zu den anderen Großbuchstaben.
Nee, der mittlere fällt raus. Daß MS das Versal-SZ nicht korrekt
behandelt, /das/ ist in der Tat ein Fehler
Seit wann gibt's das in Unicode? Wenn ich mich richtig erinnere, noch nicht sehr lange. Sprich: BUG ja, aber vermutlich eher temporärer Natur. Wird irgendwann auch noch nachgepflegt werden. In manchen skandinavischen Sprachen gibt's übrigens auch inzwischen Nachholbedarf für MS. Da wurden ebenfalls einfach Unicode-Versalien ohne Sinn und Verstand eingeführt. Also für Zeichen, die in normaler Schreibung einfach niemals groß geschrieben werden (oder vielmehr wurden).
So ist das halt, wenn jemand anders die "Standards" setzt. Da kann man nur hinterherjapsen. Der Unterschied ist: Linsux bemüht sich ja nichtmal, sondern überläßt das einfach komplett den Anwendungsprogrammierern...
Nunja, mindestens eine Milliarde Windows-Benutzer weltweit sehen das definitiv anders. Und vorher DOS-Benutzer, allerdings viel weniger als eine Milliarde. Aber etliche Millionen werden es auch damals schon gewesen sein, denen es nicht vermittelbar war, dass "FuckYou.Txt" was anderes ein soll als "fuckyou.txt".
Ok, das dein Horizont mit unflätiger Sprache endet, wissen wir ja schon.
Und deiner endet offensichtlich bei einer weltweit derart irrelevanten Sprache wie deutsch. Wieviele Leute sprechen und schreiben deutsch? Wenn's hoch kommt, vielleicht 200 Millionen. Satte 2,5% der Weltbevölkerung.
Nunja, wenn man die paar Latin-Sprachen dazu nimmt, bei denen die Caseness eine ähnliche Rolle spielt wie im Deutschen, kommen wir schon auf ein paar mehr Prozent. Relevant sind da eigentlich nur französisch, spanisch und portugiesisch. Das bissel skandinavische Zeugs kann man eher in den Skat drücken. Wie auch immer: großzügig aufgerundet sind wir hier dann alles in allem bei vielleicht bei 10% der Weltbevölkerung.
Für die restlichen 90% ist das Windows/DOS-Konzept eine Erleicherung. Genau deswegen gibt's das auch. MS wollte und will die Benutzer glücklich machen, damit die MS kaufen. Allein der Erfolg des Konzepts zeigt, dass sie das richtig analysiert haben. Schon damals, in den frühen 1980ern. Da gab's Linux noch nicht mal. Nur die Unix-Vorgänger, deren Anbieter das halt offensichtlich nicht so gut analysiert hatten. Und die ein so beschissenes Filesystem-Konzept entwickelt haben, dass sich sowas auch nachträglich nicht mehr dranstricken ließ. Das haben dann heute noch relevante Sachen wie Linux oder Free/Open-BSD geerbt.
DAS ist der eigentliche Knackpunkt.
Und du kannst diese Tasachen nicht mit dem Verweis auf "unflätige Sprache" meinerseits entkräften. Viele Leute sind durchaus intelligent genug, um diese lächerliche Ausflucht als solche zu erkennen. Gerade im konkreten Fall ist das nun wirklich sehr leicht.
Für die restlichen 90% ist das Windows/DOS-Konzept eine Erleicherung.
Genau deswegen gibt's das auch.
Ach Quatsch. Das gibt es, weil DOS sich das damals von CP/M so abgeguckt hat, und das hat es so gemacht, weil damals noch nicht jeder Computer überhaupt Kleinbuchstaben in seinem Zeichensatz hatte. Viele (inklusive DOS) haben Kleinbuchstaben in Dateinamen gar nicht unterstützt. Also im Prinzip der Gleiche Grund, aus dem es noch immer diese unsäglichen Laufwerksbuchstaben gibt: Überbleibsel aus der Computer-Steinzeit.
MS wollte und will die Benutzer glücklich machen, damit die MS kaufen.
Allein der Erfolg des Konzepts zeigt, dass sie das richtig analysiert
haben.
Klar, die Benutzer haben alle Windows nur deshalb auf ihren Computern, weil das nicht zwischen Groß- und Kleinschreibung unterscheidet. Was hast du geraucht?
Klar, die Benutzer haben alle Windows nur deshalb auf ihren Computern,
weil das nicht zwischen Groß- und Kleinschreibung unterscheidet. Was
hast du geraucht?
Aha, die idiotische Linux-Kloppertruppe auf Kriegspfad.
Nun denn: Was auch immer nun konkret die Windows (oder zuvor DOS-Benutzer) dazu gebracht hat, eben diese OS zu wählen, ist reine Spekulation.
Fakt ist hingegen, dass eben zu jeder Zeit seit den frühen 80ern des vorigen Jahrhunderts diese Wahl durch die weit überwiegende Mehrheit so getroffen wurde.
Zieht euch das einfach rein und denkt drüber nach, warum das so ist. Natürlich sind die Gründe vielfältig, darüber brauchen wir nicht diskutieren. Aber ganz sicher ist eins: Es gibt keine, wie auch immer geartete Überlegenheit des Unix-Konzepts. Ganz sicher jedenfalls und offensichtlich nicht aus Sicht der Nutzer...
Dazu kommt: MS hat das, was konzeptionell an Unix gut war, zumindest später in den NT-Kernel übernommen hat. D.h.: das ist seit Jahrzehnten auch Teil von Windows.
MS hat das, was konzeptionell an Unix gut war, zumindest später in den
NT-Kernel übernommen hat.
Das mit dem UNIX hatten sie paar Jahre vorher versucht, mit wenig Erfolg (Xenix).
NT stammt von VMS-Entwicklern.
Dass sich Millionen von Nutzern nun ausgerechnet für case insensitive entschieden hätten, halte ich für eine gewagte These: die haben einfach genommen, was sie vorgesetzt bekommen haben. Hätte Microsoft ihnen ein System vorgesetzt, das in den Dateinamen Groß- und Kleinschreibung unterscheidet, hätten sie das genauso genommen.
Immerhin kann man in Windows inzwischen wenigstens ein (bspw. vertipptes) FooBAr.txt in FooBar.txt umbenennen – lange Zeit ging das nicht, denn schließlich gibt es die Zieldatei ja schon …
Aha, die idiotische Linux-Kloppertruppe auf Kriegspfad.
Beleidigungen, wie immer…
Nun denn: Was auch immer nun konkret die Windows (oder zuvor
DOS-Benutzer) dazu gebracht hat, eben diese OS zu wählen, ist reine
Spekulation.
Du hast damit angefangen.
Fakt ist hingegen, dass eben zu jeder Zeit seit den frühen 80ern des
vorigen Jahrhunderts diese Wahl durch die weit überwiegende Mehrheit so
getroffen wurde.
Nein, das ist kein Fakt, sondern Unsinn. Die "weit überwiegende Mehrheit" hatte gar keine Wahl. Die musste das nehmen, was der Hersteller programmiert hat.
Dazu kommt: MS hat das, was konzeptionell an Unix gut war, zumindest
später in den NT-Kernel übernommen hat. D.h.: das ist seit Jahrzehnten
auch Teil von Windows.
Das meiste davon hat Microsoft allerdings nicht besonders gut umgesetzt. Symlinks gibt es seit NT, sind aber bis heute unter Windows völlig kaputt.
Fakt ist hingegen, dass eben zu jeder Zeit seit den frühen 80ern des
vorigen Jahrhunderts diese Wahl durch die weit überwiegende Mehrheit so
getroffen wurde.
Nein, das ist kein Fakt, sondern Unsinn. Die "weit überwiegende
Mehrheit" hatte gar keine Wahl. Die musste das nehmen, was der
Hersteller programmiert hat.
Ähemm.. Nö. Ich jedenfalls wurde zu keiner Zeit daran gehindert, z.B. ein Linux parallel zu einem Windows oder auch als alleiniges OS auf einem PC zu installieren (und habe das auch in all den Jahrzehnten immer wieder getan). De facto nutze ich derzeit privat 2x Linux/Windows Dual-Boot und 1x pure Linux, dienstlich (als Arbeitsrechner) 2x Linux/Windows Dual-Boot. Die Server-Landschaft ist vielfältiger. Dual-Boot gibt's da natürlich nicht, aber sowohl Windows als auch Linux in jeweils etlichen Instanzen und sogar ein einsames BSD.
Deine Aussage ist also ganz offensichtlich ein freche und obendrein überaus plumpe Lüge. Das kann man nicht beschönigen. Es gab zu keiner Zeit einen objektiven Zwang, Windows zu benutzen. Die User hatten immer die freie Wahl (Nunja, zuindest seitdem Linux überhaupt brauchbar wurde). Das würde ich mal so ab ungefährt 1994 verorten (bzw: eigentlich: "verzeitlichen").
Es gab zu keiner Zeit einen objektiven Zwang, Windows zu benutzen. Die
User hatten immer die freie Wahl (Nunja, zuindest seitdem Linux
überhaupt brauchbar wurde). Das würde ich mal so ab ungefährt 1994
verorten (bzw: eigentlich: "verzeitlichen").
Fakt ist hingegen, dass eben zu jeder Zeit seit den frühen 80ern des
vorigen Jahrhunderts diese Wahl durch die weit überwiegende Mehrheit so
getroffen wurde.
Zwischen den frühen 80ern und 1994 liegt IMHO mindestens ein Jahrzehnt.
In den frühen 80ern hatte man auf PCs im Wesentlichen die Wahl zwischen
PC-DOS, MS-DOS und CP/M-86, viel mehr war da nicht.
Was die Groß-/Kleinschreibung von Dateinamen betrifft, hat Rolf also
völlig recht:
Was die Groß-/Kleinschreibung von Dateinamen betrifft, hat Rolf also
völlig recht:
Nun, weder CP/M noch DOS kannten überhaupt das Konzept von Dateinamen. Die hatten nur 8.3-Kürzel. Windows kannte vor 1993 (Windows NT) auch keine Dateinamen, aber Windows NT brauchte noch gute acht Jahre, bis es unter dem Namen "Windows XP" im Mainstream ankan. Bis dahin konnte man ab etwa 1995 mit dem frickeligen Windows 95 immerhin auch schon in den Genuss von Dateinamen kommen, die aber nur dann funktionierten, wenn man auch 32-Bit-Programme verwendete.
Der Rest der Diskussion ist müßig, sich daran aufzugeilen, daß das selbst verwendete Betriebssystem es natürlich richtig macht, während die anderen es falsch machen hat was von praktiziertem Autismus, wenn nicht schon von religiöser Intoleranz.
Wer aus der kindlich-analen Phase raus ist, schreibt seine Software so, daß sie nicht darauf angewiesen ist, daß das Dateisystem bestimmte Eigenschaften hat, sondern mit verschiedenen Gegebenheiten zurechtkommt. Zum Beispiel auch Leerzeichen in Dateinamen oder Pfaden; es gibt Softwareentwickler, die das können, und es gibt welche, die davon gründlich überfordert sind. Die schreien dann natürlich laut rum, daß das Fehler des dummen Anwenders ist, statt einzusehen, daß es nur ihre eigene Unfähigkeit ist.
Und so bleiben uns weiterhin die schönen Flamewars erhalten, die wir schon in den 80ern (Spectrum vs. C64, Atari vs. Amiga vs. Mac etc.pp.) erleben durften.
Könnte man bloß Flamewars als Energiequelle nutzen ...
Auch das waren trotzdem Dateinamen. RSX11/M hatte 6+3 Zeichen, noch dazu Radix-50 codiert, da konnte man gar keine Kleinbuchstaben überhaupt reinschreiben, (bei CP/M konnte man es, am OS vorbei). Waren trotzdem auch Dateinamen.
Frühe UNIX-Dateisysteme konnten auch bloß 14 Zeichen, erst seit BSD UFS wurden es 255.
Zum Beispiel auch Leerzeichen in Dateinamen oder Pfaden; es gibt
Softwareentwickler, die das können, und es gibt welche, die davon
gründlich überfordert sind.
Es gibt aber auch diejenigen, die es bewusst vermeiden, weil sie wissen, dass es andere Software gibt, die damit Probleme hat. Und dann gibt es die, denen das egal ist und die es deshalb einfach den Anwender ausbaden lassen, weil sie eben in ihrer "kindlichen" Eigenschaft lieber stolz präsentieren wollen, dass sie mit Leerzeichen zurechtkommen.
Und dann gibt es
die, denen das egal ist und die es deshalb einfach den Anwender ausbaden
lassen, weil sie eben in ihrer "kindlichen" Eigenschaft lieber stolz
präsentieren wollen, dass sie mit Leerzeichen zurechtkommen.
Welches Problem verursacht eine Software, die mit Leerzeichen in Pfaden/Dateinamen zurechtkommt, bei den Anwendern?
Vielen Dank Dir Wilhem M. für das Anstoßen der Fehleranalyse und dem Mitarbeiten am Patch, sowie natürlich an Roger Sayle, Richard Biener und Segher Boessenkool, die sich des Problems gleich angenommen haben.
Ich finde es schon großartig, dass diese Modifikation nun in so kurzer Zeit Einzug in die nächste Compilerversion hält, zumal das Problem ja nur eine fehlende Optimierung betrifft. Eine wirklich großartige Open-Source-Community.
Vielen Dank Dir Wilhem M. für das Anstoßen der Fehleranalyse und dem
Mitarbeiten am Patch, sowie natürlich an Roger Sayle, Richard Biener und
Segher Boessenkool, die sich des Problems gleich angenommen haben.
Ich finde es schon großartig, dass diese Modifikation nun in so kurzer
Zeit Einzug in die nächste Compilerversion hält, zumal das Problem ja
nur eine fehlende Optimierung betrifft. Eine wirklich großartige
Open-Source-Community.
Nunja, bei den AVR8 als Target bin ich sehr froh, dass ich keine derartigen Abhängigkeiten von einem wohlwollenden, weltweit verteilten Entwicklerkollektiv habe...
Zweifelsohne hat es schon einen Reiz, wenn man in Assembler programmiert und so das Maximum an Effizienz aus dem Controller herausholt, was oft auch mit sportlichem Ehrgeiz einhergeht. Je nach Umfang des Projekts kann es mitunter aber auch Mühsam und Unübersichtlich sein.
Zweifelsohne hat es schon einen Reiz, wenn man in Assembler programmiert
und so das Maximum an Effizienz aus dem Controller herausholt
Du schreibst das so, als würde das reine Programmieren in Assembler das bereits implizieren. Tut es nicht, man kann natürlich auch in Assembler problemlos ineffizienten Code schreiben. Und die meisten werden, auch wenn das, was sie schreiben, nicht direkt ineffizient ist, noch weit entfernt sein von dem, was wirklich das mögliche Maximum ist, viel mehr, die meisten werden noch nicht mal gegen einen anständigen Compiler anstinken können.
Und dann gibt es die, denen das egal ist und die es deshalb
einfach den Anwender ausbaden lassen, weil sie eben in ihrer
"kindlichen" Eigenschaft lieber stolz präsentieren wollen,
dass sie mit Leerzeichen zurechtkommen.
Das bedeutet, daß diejenigen, die "stolz präsentieren wollen, daß sie mit Leerzeichen zurechtkommen", es "den Anwender ausbaden lassen".
Mal sehen, wie viele Jahre es dauert, bis er das in Assembler
geschrieben hat …
Monate ja, ohne Zweifel, aber sicher keine Jahre. Und nach diesen Monaten bin ich ziemlich sicher, dass ich etwas geschaffen hätte, was diesen C-Kram bezüglich der Performance sehr deutlich in den Schatten stellt.
Ich brauche nur kurz in das Kompilat zu schauen, um die erheblichen Redundanzen selbst bei -O3 zu sehen. Das könnte ich mit an Sicherheit grenzender Wahrscheinlichkeit deutlich schlagen.
Dazu kommt noch: du schummelst! Der Code enthält unzählige in (inline-) Asm programmierte Schnipsel und umschifft nur allein damit die größten Schwachen des Compilers. Wäre auch dieser ganze Kram in Plain-C programmiert, wäre das Ergebnis noch viel, viel verheerender.
Aber: Ich habe wirklich kein Interesse an Amateurfunk. Und für Anwendungen derselben Algorithmen in Bereichen, die mein Interesse erwecken, ist ein AVR8 schlicht nicht schnell genug.
Klar – weil du -O3 schlicht nicht verstanden hast.
Na dann: Klär mich auf!
Was macht diese "Optimierung" genau, was ist ihr Ziel? Und warum muß man sich den Code des Compilers anschauen, um das heraus zu finden? Sowas sollte klar und eindeutig dokumentiert sein.
Ein Werkzeug ohne brauchbare Dokumentation ist lebensgefährlich. Darauf sollte sich niemand verlassen müssen. Ich brauch' das auch nicht, weil dieses "Werkzeug" halt nur als Quelle der Erheiterung verwende (zumindest was AVR8 betrifft). Allerdings: die bezüglich AVR8 gewonnenen Erkenntnisse haben mich auch vieles gelehrt bezüglich anderer Zielarchitekturen, wo ich diesen Scheiß aus praktischen Gründen tatsächlich oft verwenden muss.
Das Fazit ist: Brauchbar gut sind Compiler allenfalls im Mainstream. Und selbst da hinken sie der Entwicklung der Hardware immer deutlich hinterher und sind auch ansonsten suboptimal, weil sie immer ihr eigenes ABI einhalten müssen. Genau dieses konstante ABI ist aber der Pitfall, der ihnen viele der in Asm mögliche Optimierungen unmöglich macht. Und dies ist wiederum beileibe nicht auf AVR8 beschränkt. Hat man z.B. bei ARM genauso.
Diese zwei Architekturen sind mein primäres Interesse im kleinen µC-Bereich. Der PC-Kram ist hingegen heute so schnell, dass die (gegenüber dem Optimum) schlechte Compiler-Performance kaum noch relevant ist. Hier ist das Problem eher die immer weiter ausuferende Verwendung immer fetterer Frameworks mit noch höherer Abstraktionslevel (betrifft allerdings auch die "dicken" ARMs).
Es hat einen Grund, dass -Os bei MCUs die beliebteste
Optimierungseinstellung ist.
Diesen Satz hatte ich komplett überlesen. Das soll aber wohl ein Witz sein? Klar kann es in wichtig sein, ob eine Anwendung überhaupt noch in in den Flash eines µC passt. Wenn man wenigstens 10000er Stückzahlen der Lösung produziert und der (bezüglich Flash-Size) nächstgrößere µC der Produktlinie exorbitant mehr kostet oder es gar keinen in der Linie mit größerem Flash mehr gibt. Ansonsten:
Viel wichtiger ist aber im Allgemeinen doch wohl, ob die Anwendung ihren Job überhaupt erledigen kann. Und das von dir gewählte Beispiel ist so eine Sache, bei der wohl eher die Rechenzeit der begrenzende Faktor ist. Das ist bei Signalverarbeitung meist so. Und der übliche Ausweg ist halt: Tabellen. Macht der Compiler bei Speed-Optimierung so und natürlich auch der kompetente Asm-Programmierer. Der kann es nur viel besser (wenn er hinreichend gut ist).
Was macht diese "Optimierung" genau, was ist ihr Ziel? Und warum muß man
sich den Code des Compilers anschauen, um das heraus zu finden? Sowas
sollte klar und eindeutig dokumentiert sein.
Das ist es. -O3 opfert Programmgröße für Speed. Loop-unrolling, massives inlining, usw. Nur ist das alles auf einem AVR ziemlich wirkungslos, da bei dem durch den (fast) echten RISC-Befehlssatz und wegen fehlenden Späßchen wie Sprungvorhersagen, mehrstufigen Caches, und all den anderen Hardwarespirenzchen größerer Prozessoren kürzerer Code halt doch schneller ist als längerer.
Daher nimmt man da -Os, und alles wird so gut, wie es halt geht.
Das war auch, was mir bekannt ist. Umso überraschender war für mich der Einwurf von Jörg W. Seines Zeichens Moderations-Macht-Missbraucher (nicht in diesem Thread (zumindest noch nicht, ich warte aber förmlich darauf), aber in anderen, das kann ich detailliert nachweisen!).
-O3 sollte also theoretisch das Maximum an Speed aus dem Compiler herausholen. Damit ist es das, mit dem sich mein Asm-Code vergleichen müsste. Genau deswegen habe ich auch das mit eben dieser Option übersetzte Kompilat als Referenz benutzt.
Und da kann ich eben ganz klar sagen: das könnte ich ganz sicher deutlich besser als es der Compiler kann.
Wie eigentlich jede AVR8-Sache. Der Compiler ist für AVR8 als Target halt einfach ziemliche Scheiße. Da spielen mehrere Sachen hinein. Zum einen ist jeder C-Compiler bei AVR8 als Zielarchitektur gleich doppelt behindert. Die mögen weder Harvard-Architektur noch native 8Bit-Verarbeitung. Was die wollen, um einigermaßen effizient zu sein ist: Die Verabeitung passiert mit (mindestestens) 16Bit. Und es gibt nur einen Addressraum. Und: Es gibt keine Nebenläufigket.
All das ist halt bei typischen AVR8-Anwendungen nicht gegeben. Deswegen ist es gerade bei dieser Architektur so überaus einfach, als Asm-Programmierer sehr deutlich besser zu sein als der Compiler...
Wenn man das aus deiner Ausdrucksweise ins normale Deutsch übersetzt,
stimmt das in etwa. gcc ist für AVR8 nicht perfekt, aber zumindest
brauchbar.
Far away from to be perfect.
Klar: Um ein paar LEDs blinkenzu lassen oder primitive state-machines mit ein wnig Input und Output zu implementieren reicht es. Und mehr hat der Herr Jörg W. auch niemals gezeigt...
Das von ihm selber gewählte Beispiel für mehr ist ja bezeichnenderweise auch nicht von ihm...
Damit ist es das, mit dem sich mein Asm-Code vergleichen müsste.
Du hast den Vergleich aber mit der Größe gemacht, nicht der Geschwindigkeit: "erheblichen Redundanzen selbst bei -O3".
Daher die Aussage: diese "Redundanzen" sind bei -O3 schlicht Absicht, nur bringen sie beim AVR aufgrund seiner Architektur kaum was. Deshalb nimmt man diese Stufe dort schlicht nicht. Wenn du auf Redundanzen vergleichen willst, dann nimm -Os: "Zusätzlich (zu Stufe 1) noch alle die Optimierungen von Stufe 2, die (normalerweise) den Code nicht größer werden lassen", "optimized for size" eben, nichts anderes heißt es.
Dass du sowieso alles viel besser kannst, wussten wir auch so schon. Nur ist halt der Aufwand dafür ein Mehrfaches. Wenn man sich das leisten kann: kein Problem, mach doch. Da ich für den nicht genutzten Flash von Microchip nichts zurück bekomme, leiste ich mir den Aufwand in der Regel eher nicht.
Das könnte ich mit an Sicherheit grenzender Wahrscheinlichkeit
deutlich schlagen.
Ich lese hier immer wieder das die heutigen Compiler Maschinencode in einer Qualität erzeugen, der nur schwer von handgeschriebenen Assembler übertroffen werden kann. Jetzt behauptest du genau das Gegenteil.
Zeig doch mal an einem typischen, nicht zu simplen, Beispiel wo ein handassemblierter Maschinencode den C-Compiler signifikant schlägt.
Selbstüberschätzung ist gerade bei Verfechtern der Assemblerprogrammierung gar nicht so selten. Im Gegensatz zu "moby" traue ich dem Programmiersprachenhasser aber wenigstens /etwas/ mehr Kompetenz zu. Vor allem kann er besser fluchen.
Und was wäre ein guter Programmierer ohne anständiges Gefluche?
Da Moby den Thread nur noch zum permanenten Wiederholen seiner Auslassungen missbraucht: hier ist eh alles geschrieben worden inzwischen, was zum Thema beiträgt.