Technik für zyklisches erhöhen eines Wertes: if versus modulo

#5678352
Lesenswert?

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).
Gast #5678516
Lesenswert?

@ 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/
Gast #5678530
Lesenswert?

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.
Gast #5678534
Lesenswert?

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
Beitrag #5678541 wurde von einem Moderator gelöscht.
#5678561
Lesenswert?

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.
#5678575
Lesenswert?

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.
#5678595
Lesenswert?

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
Gast #5678599
Lesenswert?

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.
#5678667
Lesenswert?

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.
Gast #5678693
Lesenswert?

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.
Gast #5678713
Lesenswert?

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.
Gast #5682021
Lesenswert?

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).
#5682073
Lesenswert?

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.
#5682095
Lesenswert?

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.
Gast #5682104
Lesenswert?

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.
>
1
> #define ROLLOVER( x, max )  x = ++x >= max ? 0 : x
2
>           // count up and wrap around
3
>

Und das gibt keine Sequnce-Point-Undefined-Behaviour?

(Ich hoffe, dass ist nur so hingeschrieben und nicht der originale Code, 
oder?)
#5682108
Lesenswert?

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.
Gast #5682113
Lesenswert?

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.
Gast #5682639
Lesenswert?

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.
#5682679
Lesenswert?

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.
Gast #5682724
Lesenswert?

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.
#5682738
Lesenswert?

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?
Gast #5685240
Lesenswert?

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.
#5685286
Lesenswert?

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. ;-)
Moderator #5686178
Lesenswert?

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
  else if(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.
#5686322
Lesenswert?

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
Gast #5686343
Lesenswert?

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.
Moderator Persönliche Seite #5686390
Lesenswert?

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.

Antwort schreiben

Bitte melde dich an, um einen Beitrag zu schreiben.

oder

Mit Google-Account einloggen

Die Registrierung ist kostenlos und dauert nur eine Minute.

Jetzt registrieren