Hi,
was ist eigentich "best practice" für das zyklische erhöhen eines Wertes
X von 0...(Y-1):
1
if(++X>=Y)X=0;
oder
1
X=(X+1)%Y;
Oder ist das abhängig davon, ob die CPU Division/Modulo in HW
beherrscht?
Version 2 sieht natürlich erstmal eleganter aus, dürfte aber auf fast
allen CPUs langsamer sein (sofern Y nicht 2^irgendwas ist).
@ Rolf M.
Na du wirst wohl Zeilenweise bezahlt, dann mach doch gleich
1
++x;
2
if(x==y)
3
{
4
x=0;
5
}
Haste wieder ein paar Cent verdient. Aber im erst, deine Antwort war
nicht sehr hilfreich. Hatte ich von dir nicht vor ein paar Jahren mal
was besseres gelesen?
Und zum Thema: Div bzw % wird man stets zu vermeiden versuchen, Ausnahme
wäre, wenn durch eine konstante Zweierpotenz dividiert wird, da der
Compiler dann nicht dividieren muss, sondern ein Shift verwenden kann.
Wobei man aber auch da aufpassen muss, ein reines Shift bekommt man nur
für Unsigned, für Signed generiert der Compiler in der Regel noch einen
Vorzeichentest.
Du kannst dir den Code leicht ansehen auf https://godbolt.org/
Stefan S. schrieb:> Aber im erst, deine Antwort war nicht sehr hilfreich.
Doch. Es war der kürzeste saubere weg. Alles nachfolgende ist zusammen
weniger wert. Wenn dann die Rede von sequence-points kommt, schaltet der
Up ab und macht es am Ende deswegen weiterhin falsch.
Stefan S. schrieb:> ein Shift verwenden kann
Na gut, für ein Modulo durch Zweierpotenz natürlich kein Shift, sondern
eine Maskierung durch AND.
Übrigens hatte ich schon des öfteren gelesen, dass ein Test auf
grössergleich langsamer sein kann als ein einfacher Test auf Gleichheit.
Wenn es ein Test auf Gleichheit tut, verwende ich daher jenen.
Markus M. schrieb:> Version 2 sieht natürlich erstmal eleganter aus, dürfte aber auf fast> allen CPUs langsamer sein (sofern Y nicht 2^irgendwas ist).
Du hast dir die Frage selbst beantwortet.
Stefan S. schrieb:> Stefan S. schrieb:> ein Shift verwenden kann>> Na gut, für ein Modulo durch Zweierpotenz natürlich kein Shift, sondern> eine Maskierung durch AND.>> Übrigens hatte ich schon des öfteren gelesen, dass ein Test auf> grössergleich langsamer sein kann als ein einfacher Test auf Gleichheit.> Wenn es ein Test auf Gleichheit tut, verwende ich daher jenen.
Was ist dann wenn der Wert aus iwelchen Gründen mal größer ist? >= und
== sind beide zu vermeiden. Besser ist die Prüfung auf > y-1
Stefan S. schrieb:> @ Rolf M.>> Na du wirst wohl Zeilenweise bezahlt, dann mach doch gleich> ++x;> if (x == y)> {> x = 0;> }>> Haste wieder ein paar Cent verdient.
Was ich damit sagen wollte: Ich finde es schlecht lesbar, wenn das alles
in eine Zeile gequetscht ist. Man darf auch mehrere Zeilen verwenden,
wenn das der Lesbarkeit dienlich ist.
> Aber im erst, deine Antwort war nicht sehr hilfreich.
Das ist schade. Aber deine Frage war, was "best practice" sei. Für mich
ist es das, was ich in meiner Antwort geschrieben habe. Wenn dir das
nicht hilft, kann ich ja nichts dafür.
> Und zum Thema: Div bzw % wird man stets zu vermeiden versuchen, Ausnahme> wäre, wenn durch eine konstante Zweierpotenz dividiert wird, da der> Compiler dann nicht dividieren muss, sondern ein Shift verwenden kann.
Das hängt natürlich auch vom Prozessor ab. Aber ich finde es auch von
der Lesbarkeit her weniger gut.
Peter p schrieb:> Was ist dann wenn der Wert aus iwelchen Gründen mal größer ist? >= und> == sind beide zu vermeiden. Besser ist die Prüfung auf > y-1
Welche Gründe sollten das sein außer einem Fehler im Programm oder einem
Fehler in der Hardware? Für ersteres würde ich dann lieber ein assert
oder sowas verwenden, damit man den Fehler auch sieht.
Klar wäre es sinnvoll sicherzustellen dass solche Fehler nicht
auftreten. Aber es ist schlicht die schnellste und sicherste Variante,
also warum was anderes machen...
Ich bin kein Freund davon, Fehler zu verstecken. Denn das fällt einem
dann irgendwann später auf den Fuß und erschwert das Debugging. Also ja,
ein >= ist von mir aus ok, aber dann sollte der Fall > auch explizit als
Fehler behandelt werden, denn der wird ja nur erreicht, wenn irgendwas
schief gelaufen ist.
Danke, damit wäre meine Annahme bestätigt.
Zur Diskussion um die Syntax: Ich schreibe es normalerweise auch nicht
immer in eine Zeile, habe es nur in eine Zeile gequetscht um die
(syntaktische) Länge als Entscheidungskriterium auszublenden, das
Gegenteil habe ich erreicht :-)
Allgemein versuche ich Zuweisungsoperationen (also auch Increment /
Decrement) aus den IF-Statements rauszuhalten, aber das ist nunmal
Geschmackssache.
PS:
Die Seite https://godbolt.org/ "Comiler Explorer" ist cool! Da z.B. der
Cortex-M4 bei der Division keinen Rest zurückgibt, wird modulo offenbar
über Multiplikation realisiert :) Die Variante mit dem "if" ist dagegen
super kurz (modulo 10 im Beispiel):
add r0, r0, #1
cmp r0, #10
moveq r0, #0
Rolf M. schrieb:> Ich bin kein Freund davon, Fehler zu verstecken. Denn das fällt> einem dann irgendwann später auf den Fuß und erschwert das Debugging.> Also ja, ein >= ist von mir aus ok, aber dann sollte der Fall > auch> explizit als Fehler behandelt werden, denn der wird ja nur erreicht,> wenn irgendwas schief gelaufen ist.
Manche nennen es Fehler verstecken, andere neben es robuste
Programmierung. Einer der letzteren war damals mein Prof für Embedded
Systems. Daran orientiert ich mich erstmal.
Peter p schrieb:> Weil man y-1 einmal vor der Schleife bestimmen kann und > i. A.> schneller ist als >=. Könnte aber auch veraltet sein.
Das hat dann aber mit Hochsprachenprogrammierung nichts mehr zu tun.
Solch Optimierungen kann der Compiler.
Peter p schrieb:> Rolf M. schrieb:>> Ich bin kein Freund davon, Fehler zu verstecken. Denn das fällt>> einem dann irgendwann später auf den Fuß und erschwert das Debugging.>> Also ja, ein >= ist von mir aus ok, aber dann sollte der Fall > auch>> explizit als Fehler behandelt werden, denn der wird ja nur erreicht,>> wenn irgendwas schief gelaufen ist.>> Manche nennen es Fehler verstecken, andere neben es robuste> Programmierung. Einer der letzteren war damals mein Prof für Embedded> Systems. Daran orientiert ich mich erstmal.
Naja, wie gesagt: Ein Fehler ist passiert. Wenn der Wert falsch ist, hat
das Programm schon Mist gebaut. Man hat bei >= immerhin den Vorteil,
dass sich die Auswirkungen des Fehlers nicht weiter fortsetzen. Ein
Fehler ist aber trotzdem passiert, dessen muss man sich eben bewusst
sein.
Peter p schrieb:> Weil man y-1 einmal vor der Schleife bestimmen kann und > i. A.> schneller ist als >=. Könnte aber auch veraltet sein.
Mir wäre keine Architektur bekannt, wo das so ist.
Peter p schrieb:> Du diskutierst gerne was?
Manchmal schon, aber nicht mit Dir!
Nach einigem Nachdenken könnte ich mir vorstellen, dass auf einigen
Architekturen x == 0 schneller ist als x >= 0 bzw x <= 0. Grund: Wenn x
in ein Register geladen wird, wird womöglich schon das ZERO Flag
gesetzt, so dass ein nachfolgendes CMP bei x == 0 entfallen kann. Leider
nur eine Vermutung, bei x86 ist das wohl nicht so, da Mov keinerlei
Flags setzt.
Rolf M. schrieb:> Ich bin kein Freund davon, Fehler zu verstecken.
Ich eher auch nicht.
Und nicht nur, dass man durch ein >= wo ein == stehen sollte einen
womöglichen Logikfehler versteckt: Man macht es dem Compiler und einem
Leser des Codes schwieriger die tatsächliche Logik zu verstehen. Der
Compiler kann dann womöglich schlechter optimieren oder Fehler erkennen.
Das mit dem Bitkippen durch radioaktive Strahlung -- da wird ein >= den
Marsrover auch nicht retten.
Stefan S. schrieb:> Und nicht nur, dass man durch ein >= wo ein == stehen sollte einen> womöglichen Logikfehler versteckt: Man macht es dem Compiler und einem> Leser des Codes schwieriger die tatsächliche Logik zu verstehen.
Genauso ist es. Hätte ich nicht besser ausdrücken können.
Und wenn man den Wert tatsächlich überprüfen will/muss, dann macht man
das dort, wo der Wert verwendet wird (z.B. mit einem Assert).
Rolf M. schrieb:> Man hat bei >= immerhin den Vorteil,> dass sich die Auswirkungen des Fehlers nicht weiter fortsetzen.
Genau das ist ein sehr wichtiger Punkt. Ich sichere damit ab, daß nur
die Funktion selber Fehler macht.
Bei einem == dagegen, kann ich mir einen völlig anderen Speicherbereich
zerstören und lege mir dann richtig die Karten.
Der Test auf >= ist defensive Programmierung und daher eindeutig
vorzuziehen.
Ich hab schonmal ewig und 3 Tage Fehler in der falschen Task gesucht,
daher vermeide ich wildgewordene Pointer, wo immer es geht.
Peter D. schrieb:> Rolf M. schrieb:>> Man hat bei >= immerhin den Vorteil,>> dass sich die Auswirkungen des Fehlers nicht weiter fortsetzen.>> Genau das ist ein sehr wichtiger Punkt.
Naja, geht so.
> Ich sichere damit ab, daß nur die Funktion selber Fehler macht.> Bei einem == dagegen, kann ich mir einen völlig anderen Speicherbereich> zerstören und lege mir dann richtig die Karten.
Wenn der >-Teil anspricht, war der Wert bereits zu groß, und die ganzen
potenziellen Probleme, die daraus entstehen können, sind bereits da. Das
Kind ist schon in den Brunnen gefallen. Mit >= statt == sorge ich
lediglich dafür, dass nicht noch ein zweites Kind reinfällt.
A. S. schrieb:> Und das gibt keine Sequnce-Point-Undefined-Behaviour?
Nö. Der ternäre Operator hat nach der Bedingung einen Sequenzpunkt.
Peter D. schrieb:> Ich hab schonmal ewig und 3 Tage Fehler in der falschen Task gesucht,> daher vermeide ich wildgewordene Pointer, wo immer es geht.
Daher ja die Assertion oder andere Meldung eines Fehlers, wenn x mal >
max sein sollte.
Peter D. schrieb:> Ich hab mir dafür ein Macro geschrieben, da man es häufig braucht.> Der AVR-GCC macht daraus auch kürzeren Code, als mit if.#define> ROLLOVER( x, max ) x = ++x >= max ? 0 : x> // count up and wrap around
Dieses Macro macht betroffen...
Mal von den fehlenden Klammern abgesehen, erfolgt Zugriff und
Modifikation einer Variablen in einem Ausdruck.
Rolf M. schrieb:> Nö. Der ternäre Operator hat nach der Bedingung einen Sequenzpunkt.
Ja, das habe ich völlig übersehen. Und im Gegensatz zu sonstigen
#defines finde ich hier tatsächlich kaum relvante Fälle, wo die fehlende
Klammerung ein (unerkanntes) Problem darstellt.
Rolf M. schrieb:> Was ich damit sagen wollte: Ich finde es schlecht lesbar, wenn das alles> in eine Zeile gequetscht ist. Man darf auch mehrere Zeilen verwenden,> wenn das der Lesbarkeit dienlich ist.
Treibt man das weiter, bekommt man zwar sehr viele, dafür aber
aussagearme Zeilen. Also ähnlich wie bei Assembler. :-)
Peter p schrieb:> Weil man y-1 einmal vor der Schleife bestimmen kann und > i. A.> schneller ist als >=. Könnte aber auch veraltet sein.
Das ist Aufgabe des Compilers, der sollte das auf den betroffenen
Architekturen können. Ob die Vergleiche unterschiedlich schnell sind,
hängt von der Architektur ab.
Stefan S. schrieb:> Und nicht nur, dass man durch ein >= wo ein == stehen sollte einen> womöglichen Logikfehler versteckt: Man macht es dem Compiler und einem> Leser des Codes schwieriger die tatsächliche Logik zu verstehen.
Ich weiß ja nicht.
Nehmen wir mal einen Software-Timer. Der zählt immer brav von 0 bis N
hoch und soll nicht überlaufen. Soweit, sogut. Euer Argument
funktioniert, solange ich den Vergleichswert N niemals bei aktivem Timer
ändere.
Andernfalls habe ich jetzt einen subtilen Bug in meinem Programm, der
möglicherweise sehr lange unentdeckt bleiben und viel Schaden anrichten
kann, dazu noch schwer zu debuggen ist.
Um den Bug zu vermeiden, brauche ich jetzt zusätzliche Logik in der
Änderungsfunktion, was möglicherweise neue Bugs erschafft. Hätte ich
defensiv programmiert, wäre das nicht nötig.
Nun ist mein System überlastet und kann nicht schnell genug auf den
Timer reagieren und verpasst einen Timerwert. Plötzlich wartet mein
System nicht einen Tick zu lange, sondern im Zweifelsfall UINT32_MAX,
bevor es wieder reagiert. Das kommt einem Absturz sehr nahe. Hätte ich
defensiv programmiert, wäre das nicht passiert.
Irgendeine schlaue Nase kam nun auf die Idee, den Zählerwert auf float
umzustellen, weil die neue Version der Hardware dafür geeignet ist und
die Anwendung davon profitiert. Plötzlich triggert der Timer garnicht,
weil man Float-Werte nicht sinnvoll auf Gleichheit testen kann. Hätte
ich defensiv programmiert, wäre das kein Problem.
Und weil mein System vorerst nur ein kleiner Controller ist, ist eine
sichtbare Fehlermeldung nicht möglich bzw. würde den Nutzer nur
verwirren. Ob das System nun direkt abstürzt (weil ein abort() triggert)
oder ob es sich aufhängt (weil der Timer nicht mehr timert),
interessiert den Kunden nicht weiter - sauer ist er in beiden Fällen.
Mir ist Code lieber, der keine Annahmen über seinen Einsatz treffen
muss, weil er defensiv formuliert wurde und sich damit immer
deterministisch verhält. Dazu gehört auch, dass Funktionen anstatt
ordentlicher Fehlererkennung lieber so geschrieben sein sollten, dass
sie grundsätzlich nicht fehlschlagen können. Sowas geht dann auch in die
Richtung API-Design.
Gut erklärt! So sehe ich das auch. Ehrlich gesagt verstehe ich auch
nicht, wieso ein >= die Logik schwerer zu verstehen macht. Ich würde
umgekehrt bei einem == immer stutzig werden (aber ich bin auch auf
defensive Programmierung gepolt). Spätestens wenn der Timer das erste
Mal irgendein kleines Problem hätte, würde ich da sofort testweise ein
>= draus machen und das dann auch so lassen, selbst wenn das Problem
woanders lag.
Hat jemand noch mehr sinnvolle Beispiele für defensive Programmierung?
Da sind vielleicht auch für Anfänger hilfreiche Sachen dabei.
S. R. schrieb:> Rolf M. schrieb:>> Was ich damit sagen wollte: Ich finde es schlecht lesbar, wenn das alles>> in eine Zeile gequetscht ist. Man darf auch mehrere Zeilen verwenden,>> wenn das der Lesbarkeit dienlich ist.>> Treibt man das weiter, bekommt man zwar sehr viele, dafür aber> aussagearme Zeilen. Also ähnlich wie bei Assembler. :-)
Klar - man kann alles ad absurdum führen. Ich sage ja nicht, dass man so
viele Zeilen wie möglich nutzen soll, sondern so viele wie sinnvoll. Und
oben finde ich es sinnvoll, nicht alles in eine Zeile zu schreiben.
> Nehmen wir mal einen Software-Timer. Der zählt immer brav von 0 bis N> hoch und soll nicht überlaufen. Soweit, sogut. Euer Argument> funktioniert, solange ich den Vergleichswert N niemals bei aktivem Timer> ändere.>> Andernfalls habe ich jetzt einen subtilen Bug in meinem Programm, der> möglicherweise sehr lange unentdeckt bleiben und viel Schaden anrichten> kann, dazu noch schwer zu debuggen ist.>> Um den Bug zu vermeiden, brauche ich jetzt zusätzliche Logik in der> Änderungsfunktion, was möglicherweise neue Bugs erschafft. Hätte ich> defensiv programmiert, wäre das nicht nötig.
Dann kann es aber einen Zyklus geben, in dem der Zähler weder bis zum
alten N, noch bis zum neuen N zählt, sondern so weit, wie er eben gerade
war, als N geändert wurde. Das kann auch ein sehr subtiler Bug sein.
> Und weil mein System vorerst nur ein kleiner Controller ist, ist eine> sichtbare Fehlermeldung nicht möglich bzw. würde den Nutzer nur> verwirren.
Ob ihn das nun mehr verwirrt als ein Fehlverhalten ohne Meldung?
Aber es war auch nicht so sehr als Meldung für den Benutzer gedacht,
sondern für den Entwickler. Der soll ja wenigstens merken, dass hier ein
Zustand eingetreten ist, den es eigentlich gar nicht geben dürfte.
Ein Benutzer kann mit einem "Fehler: MyCycleCounter ist 42. Das hätte
nicht passieren dürfen!" oder einer LED, die einem das per Morsecode
mitteilt, ja nun auch nicht wirklich was anfangen.
> Dazu gehört auch, dass Funktionen anstatt ordentlicher Fehlererkennung> lieber so geschrieben sein sollten, dass sie grundsätzlich nicht> fehlschlagen können.
Also so, dass es keinen falschen Zählerwert geben kann und somit ein
Vergleich auf >= gar nicht nötig wäre?
555nase schrieb:> Hat jemand noch mehr sinnvolle Beispiele für defensive Programmierung?
Ja, natürlich.
Die erste Regel lautet, daß man sich nie drauf verlassen sollte, daß "am
Anfang ja sowieso alles genullt" sei.
Ich hatte da mal ein böses Fehlverhalten des FatFS von Chan nach Wechsel
der SD-Karte. Das kam daher, daß Chan sich voll auf "ist am Anfang
genullt" verlassen hat. War vor Jahren, kann sein, daß er das inzwischen
entbugt hat.
W.S.
W.S. schrieb:> 555nase schrieb:>> Hat jemand noch mehr sinnvolle Beispiele für defensive Programmierung?>> Ja, natürlich.> Die erste Regel lautet, daß man sich nie drauf verlassen sollte, daß "am> Anfang ja sowieso alles genullt" sei.>> Ich hatte da mal ein böses Fehlverhalten des FatFS von Chan nach Wechsel> der SD-Karte. Das kam daher, daß Chan sich voll auf "ist am Anfang> genullt" verlassen hat. War vor Jahren, kann sein, daß er das inzwischen> entbugt hat.>> W.S.
Falls das so ist, dann sollte man die C-Runtime entbuggen und nicht
darauf hoffen, daß weimal nullen mehr Bits löscht. So ein
memset(__bss_start,0,__bss_end-__bss_start)
ist ja nicht so kompliziert.
Man könnte das aber ja auch das 2. Mal auch nicht hinbekommen. Was dann,
3x(/4x/5x) nullen?
Wenn man sich wirklich auf keinen Sprachstandard verlassen will, dann
sollte man Compiler meiden und seine Programme Bit-weise
zusammenklöppeln. Wenn man das sogar wörtlich nimmt erhält man das
einzig optisch verifizierbare ROM. Hat ja sogar für die Mondladung
gereicht. ;-)
W.S. schrieb:> Die erste Regel lautet, daß man sich nie drauf verlassen sollte, daß "am> Anfang ja sowieso alles genullt" sei.
Das hast Du schon mal erzählt, aber das bedeutet, daß der Compiler bzw.
die Laufzeitumgebung/der Starupcode effektiv kaputt ist. Und dann kann
man sich sowieso auf überhaupt gar nichts verlassen.
W.S. schrieb:> Die erste Regel lautet, daß man sich nie drauf verlassen sollte, daß "am> Anfang ja sowieso alles genullt" sei.
Ja, man sollte generell immer mit dem Schlimmsten rechnen. Deswegen
schreibt man statt
1
c=a+b;
getreu dem Motto "doppelt genäht hält besser" folgendes:
1
c1=a+b;
2
c2=a+b;
3
c3=a+b;
4
5
if(c1==c2||c1==c3)
6
c=c1;
7
elseif(c2==c3)
8
c=c2;
9
else{
10
fprintf(stderr,"Schwerer Fehler\n");
11
exit(1);
12
}
13
14
// c enthält jetzt mit recht hoher Wahrscheinlichkeit das richtige
15
// Ergebnis
Damit werden sporadisch auftretende Additionsfehler elegant unter den
Teppich gekehrt ;-)
Anmerkung: Optimierungen durch den Compiler müssen deaktiviert werden.
Yalu X. schrieb:> mit recht hoher Wahrscheinlichkeit
Da gibt's übrigens eine eigene Programmiersprache, die sich dieser
Thematik widmet. In Java2k werden Instruktionen nur mit einer gewissen
Wahrscheinlichkeit ausgeführt. Da kann man dann damit experimentieren,
wie man es mit möglichst wenig Rechenaufwand schafft, die
Wahrscheinlichkeit zu maximieren.
Und zusätzlich werden übrigens statt unleserlichen Bezeichnern viel
besser lesbare Magic Numbers verwendet:
https://de.wikipedia.org/wiki/Liste_von_Hallo-Welt-Programmen/Sonstige#Java2K
Carl D. schrieb:> Falls das so ist, dann sollte man die C-Runtime entbuggen und nicht> darauf hoffen, daß weimal nullen mehr Bits löscht. So ein> memset(__bss_start,0,__bss_end-__bss_start)> ist ja nicht so kompliziert.> Man könnte das aber ja auch das 2. Mal auch nicht hinbekommen. Was dann,> 3x(/4x/5x) nullen?
Du hast den Knackpunkt nicht verstanden: Es ist völlig NORMAL, daß man
bei laufendem Controller ne SD-Karte herausnehmen und ne andere
reinstecken kann, ohne daß der ganze Controller durch's Reset gehen muß.
Stell dir doch mal vor, du müßtest deinen PC jedesmal runterfahren, wenn
du nen USB-Stick raus und einen anderen reinstecken willst.
W.S.
W.S. schrieb:> Du hast den Knackpunkt nicht verstanden: Es ist völlig NORMAL, daß man> bei laufendem Controller ne SD-Karte herausnehmen und ne andere> reinstecken kann, ohne daß der ganze Controller durch's Reset gehen muß.
Eben. Also ist der Controller nicht in den Reset gegangen und es gab
auch keine Notwendigkeit, Static Data wieder zu nullen. Du schilderst
ein Phantom.