Forum: Compiler & IDEs Strukturierte Programmierung: Anti-Switch-Case


von Walter T. (nicolas)


Lesenswert?

Hallo zusammen,

wenn ich in eine Schleife oder einen Quelltext-Teil an einer bestimmten 
Stelle hereinspringen will, bietet sich switch-case an. Wie gehe ich 
denn bei strukturierter Programmierung vor, wenn ich aus einem 
Programmteil herausspringen will?

Als Beispiel habe ich mal einen UTF-8-Dekoder gemacht:
1
    /** UTF8-Zeichenlaenge anhand des ersten Bytes ermitteln
2
     * Die Laenge eines UTF-8-Zeichens (1 bis 4 Byte) ist im ersten Zeichen einkodiert.
3
     *       0xxxxxxx -> 1 Byte
4
     *       110xxxxx -> 2 Bytes
5
     *       1110xxxx -> 3 Bytes
6
     *       11110xxx -> 4 Bytes
7
     *
8
     * Die clz() instruction benoetigt einen Takt auf dem Cortex M3.
9
     *
10
     * @param[in] Zeiger auf erstes Byte eines UTF-8-Zeichens
11
     * @return Anzahl der Bytes des UTF-8-Zeichens */
12
    int_fast8_t utf8bytes(const char *s0)
13
    {
14
        uint32_t a = *s0;
15
        a <<= 24;
16
        int_fast8_t r = __builtin_clz(~a);
17
        r = r==0 ? 1 : r;
18
        return r;
19
    }
20
21
22
    /** Konvertierung UTF8-Zeichen in Unicode
23
     *
24
     * @param[in] Zeiger auf erstes Byte eines UTF-8-Zeichens
25
     * @param[out] Unicode
26
     * @return Anzahl der konsumierten Bytes */
27
    int_fast8_t utf8ToUnicode(uint32_t *unicode, const char *s)
28
    {
29
        int_fast8_t nBytes = utf8bytes(s);
30
        uint32_t r = 0;
31
32
        switch(1)
33
        {
34
            case 1:
35
36
               /* 1-Byte-Bereich U+0000 bis U+007F */
37
                r = *s++;
38
                if( nBytes <= 1 ) break;
39
40
41
                /* 2-Byte-Bereich U+0080 bis U+07FF */
42
                r <<= 6;
43
                r |= (*s++) & 0x3F;
44
                r &= 0x000003FF;  /* Erste zwei Bits ausmaskieren */
45
                if( nBytes <= 2 ) break;
46
47
48
                /* 3-Byte-Bereich U+0800 bis U+FFFF */
49
                r <<= 6;
50
                r |= (*s++) & 0x3F;
51
                r &= 0x0000FFFF;  /* Erste drei Bits ausmaskieren */
52
                if( nBytes <= 3 ) break;
53
54
55
                /* 4-Byte-Bereich U+10000 bis U+10FFFF */
56
                r <<= 6;
57
                r |= (*s++) & 0x3F;
58
                r &= 0x01FFFFFF;  /* Erste vier Bits ausmaskieren */
59
        }
60
        *unicode = r;
61
        return nBytes;
62
    }
Ich habe mich dafür entschieden, dass switch-break an dieser Stelle das 
am wenigsten häßliche mir bekannte Konstrukt ist. Das ist schon beinahe 
goto-esk.

Man könnte den zweiten Teil auch mit einer while-Schleife machen und 
neben dem Schleifenzähler auch die Maske mitschieben.

Oder geht es noch besser?

: Bearbeitet durch User
von Oliver S. (oliverso)


Lesenswert?

Walter T. schrieb:
> Oder geht es noch besser?

Das kommt ganz auf deine Definition von "gut" an. Ich würde bei dem Code 
fragen: geht es noch schlechter?

Das ist auf halben Weg zum Obfuscation-C-Contest, ohne jeglichen 
Mehrwert durch all die grotesken Konstrukte. Krampfhaftes vermeiden von 
if ist weder cool noch hipp noch modern, noch irgendetwas anderes, wenn 
if genau das ausdrückt, was es ausdrücken soll.

Oliver

: Bearbeitet durch User
von Walter T. (nicolas)


Lesenswert?

Wie würdest Du es unobsfuskiert ausdrücken?

von Thomas F. (tommf)


Lesenswert?

1
    int_fast8_t utf8ToUnicode(uint32_t *unicode, const char *s)
2
    {
3
        int_fast8_t nBytes = utf8bytes(s);
4
        uint32_t r = 0;
5
        if( nBytes >= 1 )
6
        {
7
           /* 1-Byte-Bereich U+0000 bis U+007F */
8
           r = *s++;
9
        }
10
        if( nBytes >= 2 )
11
        { 
12
           /* 2-Byte-Bereich U+0080 bis U+07FF */
13
           r <<= 6;
14
           r |= (*s++) & 0x3F;
15
           r &= 0x000003FF;  /* Erste zwei Bits ausmaskieren */
16
        }
17
        if( nBytes >= 3 )
18
        {
19
           /* 3-Byte-Bereich U+0800 bis U+FFFF */
20
           r <<= 6;
21
           r |= (*s++) & 0x3F;
22
           r &= 0x0000FFFF;  /* Erste drei Bits ausmaskieren */
23
        }
24
        if ( nBytes >= 4 )
25
        {
26
           /* 4-Byte-Bereich U+10000 bis U+10FFFF */
27
           r <<= 6;
28
           r |= (*s++) & 0x3F;
29
           r &= 0x01FFFFFF;  /* Erste vier Bits ausmaskieren */
30
        }
31
        *unicode = r;
32
        return nBytes;
33
    }

von Oliver S. (oliverso)


Lesenswert?

Einfach das hinschreiben, was gemeint ist.
1
   /* 1-Byte-Bereich U+0000 bis U+007F */
2
   r = *s++;
3
4
   /* 2-Byte-Bereich U+0080 bis U+07FF */
5
   if( nBytes >1 ){
6
   r <<= 6;
7
//...
8
  }
9
 /* 3-Byte-Bereich U+0800 bis U+FFFF */
10
   if( nBytes >2 ){
11
   r <<= 6;
12
//...
13
  }
14
// usw.

Oliver
P.S. Thomas tippte schneller ;)

: Bearbeitet durch User
von Walter T. (nicolas)


Lesenswert?

Stimmt. Das ist naheliegend. Manchmal sieht man den Wald nicht mehr. Die 
Abbruchbedingung nur einmal zu prüfen wird wohl nicht hinzubekommen 
sein.

von Rolf M. (rmagnus)


Lesenswert?

Walter T. schrieb:
> wenn ich in eine Schleife oder einen Quelltext-Teil an einer bestimmten
> Stelle hereinspringen will, bietet sich switch-case an.

Tut es das?

> switch(1)
>         {
>             case 1:

Was zum Geier soll das denn sein?

> Ich habe mich dafür entschieden, dass switch-break an dieser Stelle das
> am wenigsten häßliche mir bekannte Konstrukt ist. Das ist schon beinahe
> goto-esk.

Man muss sich nicht zwanghaft irgendwelche kranken Konstrukte ausdenken, 
nur um auf Biegen und Brechen goto zu vermeiden, weil man halt mal 
indoktriniert wurde, dass das die Saat des Bösen sei.
Das hier ist einer der beiden Fälle, die ich kenne, in denen goto 
durchaus sinnvoll ist.

: Bearbeitet durch User
von JayLow (Gast)


Lesenswert?

1
do
2
{
3
...
4
} while(0);

von Oliver S. (oliverso)


Lesenswert?

Walter T. schrieb:
> Die
> Abbruchbedingung nur einmal zu prüfen wird wohl nicht hinzubekommen
> sein.

Wenn du unbedingt ein switch willst, dann nimm ein switch (nBytes), und 
ordne die Fälle absteigend ohne break an (fallthrough). Braucht etwas 
Umformulierung der Berechnung, aber das kriegst du hin. Das ist dann 
aber immer noch nicht "gut".

Oliver

von NichtWichtig (Gast)


Lesenswert?

JayLow schrieb:
>
1
> do
2
> {
3
> ...
4
> } while(0);
5
>

+1

von Walter T. (nicolas)


Lesenswert?

Ja, ein bischen Abstand tut gut. Eigentlich brauche ich die Einsen am 
Anfang gar nicht zu zählen. So geht es auch:
1
    /** Konvertierung UTF8-Zeichen in Unicode
2
     *
3
     * @param[in] Zeiger auf erstes Byte eines UTF-8-Zeichens
4
     * @param[out] Unicode
5
     * @return Anzahl der konsumierten Bytes */
6
    int_fast8_t utf8ToUnicode(uint32_t *unicode, const char *s)
7
    {
8
        assert( unicode != NULL );
9
        assert( s != NULL );
10
11
        /* char kann signed sein */
12
        uint_fast8_t byte0 = (uint8_t) *s++;
13
14
        /* Byte0 = 0xxxxxxx -> 1 Byte
15
         * 1-Byte-Bereich U+0000 bis U+007F */
16
        uint32_t r = byte0;
17
        int_fast8_t nBytes = 1;
18
19
20
        if( byte0 >= 0xC0 )
21
        {
22
            /* Byte0 == 110xxxxx -> 2 Bytes, U+0080 bis U+07FF */
23
            r <<= 6;
24
            r |= (*s++) & 0x3F;
25
            r &= 0x000003FF;  /* Erste zwei Bits ausmaskieren */
26
            nBytes++;
27
        }
28
29
        if( byte0 >= 0xE0 )
30
        {
31
            /* Byte0 == 1110xxxx -> 3 Bytes, U+0800 bis U+FFFF */
32
            r <<= 6;
33
            r |= (*s++) & 0x3F;
34
            r &= 0x0000FFFF;  /* Erste drei Bits ausmaskieren */
35
            nBytes++;
36
        }
37
38
        if( byte0 >= 0xF0 )
39
        {
40
            /* Byte0 == 11110xxx 4 Bytes U+10000 bis U+10FFFF */
41
            r <<= 6;
42
            r |= (*s++) & 0x3F;
43
            r &= 0x01FFFFFF;  /* Erste vier Bits ausmaskieren */
44
            nBytes++;
45
        }
46
        *unicode = r;
47
        return nBytes;
48
    }

: Bearbeitet durch User
von Hannes J. (Firma: _⌨_) (pnuebergang)


Lesenswert?

Rolf M. schrieb:
>> switch(1)
>>         {
>>             case 1:
>
> Was zum Geier soll das denn sein?

Verzweifelter Versuch dass er break verwenden kann, damit er kein 
"böses" return mitten aus einer Funktion verwenden muss.

Was vermutlich geht (ich habe jetzt keine Lust mir die Masken für r 
anzusehen), ist einfach eine Schleife, auch wenn die Berechnung etwas 
unsymmetrisch ist
1
int_fast8_t nBytes = utf8bytes(s);
2
uint32_t r = 0;
3
mask1 = ...
4
mask2 = ...
5
6
for(i = 0; i < nBytes; i++) {
7
...
8
}

Nebenbei ist die Bezeichnung der Konvertierung falsch

>     /** Konvertierung UTF8-Zeichen in Unicode

UTF-8 ist schon Unicode, im Transformation Format 8. Was er versucht ist 
den Unicode Codepoint auszurechnen.

: Bearbeitet durch User
von Walter T. (nicolas)


Lesenswert?

Walter T. schrieb:
> So geht es auch:

Hng. Der strunzdumme Compiler merkt nicht, dass die vorherigen 
Bedingungen schon enthalten sind und er eigentlich bis zum Ende springen 
kann, ohne alles jeweils einzeln vergleichen zu müssen. Also müssen die 
if-Anweisungen geschachtelt werden oder doch wieder Goto oder ein 
Ersatz.

: Bearbeitet durch User
von fizzbuzz (Gast)


Lesenswert?

if und if-else + andere Reihenfolge.
1
//>0xF0
2
if(a>240){
3
//bla bla
4
{
5
else if(a>224){
6
//bla bla
7
}
8
else if(a>192){
9
//bla bla
10
}

https://wiki.c2.com/?FizzBuzzTest

von Walter T. (nicolas)


Lesenswert?

fizzbuzz schrieb:
> if und if-else + andere Reihenfolge.

Stimmt. Eigentlich ist es ja total egal, in welcher Reihenfolge die 
Bytes aus dem Array gelesen werden.

von mh (Gast)


Lesenswert?

Walter T. schrieb:
> Hng. Der strunzdumme Compiler merkt nicht, dass die vorherigen
> Bedingungen schon enthalten sind und er eigentlich bis zum Ende springen
> kann, ohne alles jeweils einzeln vergleichen zu müssen. Also müssen die
> if-Anweisungen geschachtelt werden oder doch wieder Goto oder ein
> Ersatz.

Ich bin mir nicht sicher was genau du meinst, aber hast du Optimierungen 
aktiviert?

von Walter T. (nicolas)


Lesenswert?

mh schrieb:
>
> Ich bin mir nicht sicher was genau du meinst, aber hast du Optimierungen
> aktiviert?

Die folgenden Varianten sind funktionell identisch, weil die Vergleiche 
aufsteigend sortiert sind:
1
if( byte0 >= 0xC0 )
2
{
3
    ...
4
}
5
if( byte0 >= 0xE0 )
6
{
7
    ...
8
}
9
if( byte0 >= 0xF0 )
10
{
11
    ...
12
}
13
return
14
15
16
17
if( byte0 >= 0xC0 )
18
{
19
    ...
20
    if( byte0 >= 0xE0 )
21
    {
22
        ...
23
        if( byte0 >= 0xF0 )
24
        {
25
            ...
26
        }
27
    }
28
}
29
return
Tatsächlich wird mit -O1 aber in der ersten Variante der Vergleich 
dreimal vorgenommen, wenn er schon beim ersten Mal schiefgegangen ist. 
Entsprechend wird dreimal gesprungen.

: Bearbeitet durch User
von mh (Gast)


Lesenswert?

Walter T. schrieb:
> Tatsächlich wird mit -O1 aber in der ersten Variante der Vergleich
> dreimal vorgenommen, wenn er schon beim ersten Mal schiefgegangen ist.
> Entsprechend wird dreimal gesprungen.

Welcher Compiler?

von Walter T. (nicolas)


Lesenswert?


von Rolf M. (rmagnus)


Lesenswert?

-O1 ist halt minimale Optimierung. Bei -O2 macht er es. Außerdem ist gcc 
5.4 schon recht alt.

von Walter T. (nicolas)


Lesenswert?

Sehr merkwürdig. Selbst beim neuesten ARM-GCC auf der höchsten 
Optimierungsstufe sind die beiden nicht äquivalent:

https://godbolt.org/z/7M7Y9h89K

Bis hin zu dem Kuriosum, dass die ersten beiden Zeilen unterschiedlich 
übersetzt werden, weil ein Block mit Textzeigern über die 
130-Bytes-Grenze wandert.

von Experte (Gast)


Lesenswert?

1
static int8_t decode((uint32_t *unicode, const char *s, char mask, int8_t additional_bytes)
2
{
3
  *unicode = (uint32_t)(*s++ & mask);
4
5
  for (int8_t i = 0; i < additional_bytes; i++)
6
    *unicode = (*unicode << 6) | (uint32_t)(*s++ & 0x3f);
7
8
  return 1 + additional_bytes;
9
}
10
11
int8_t utf8ToUnicode(uint32_t *unicode, const char *s)
12
{
13
  if (!(*s & 0x80))
14
    return decode(unicode, s, 0x7f, 0);
15
  
16
  else if (!(*s & 0x20)) 
17
    return decode(unicode, s, 0x1f, 1);
18
  
19
  else if (!(*s & 0x10))
20
    return decode(unicode, s, 0x0f, 2);
21
  
22
  else
23
    return decode(unicode, s, 0x07, 3);
24
}

von Yalu X. (yalu) (Moderator)


Lesenswert?

Wie wär's mit einer Schleife statt der If-Kaskade?

1
uint32_t utf8decode(uint8_t bytes[]) {
2
  uint8_t first = *bytes++;
3
  uint32_t result = first, mask = 0xffffff80;
4
5
  if (first & 0x80) {
6
    first <<= 1;
7
    while (first & 0x80) {
8
      result = result << 6 | *bytes++ & 0x3f;
9
      first <<= 1;
10
      mask <<= 5;
11
    }
12
  }
13
  result &= ~mask;
14
15
  return result;
16
}

von Experte (Gast)


Lesenswert?

Yalu X. schrieb:
> mask <<= 5;

Clever!


> Wie wär's mit einer Schleife statt der If-Kaskade?

Wie gesagt, gut gesehen, gefällt mir aber nicht.

Ich mag es, wenn Code den Fällen der Spezifikation entspricht. Lässt 
sich in funktionalen Sprachen sehr schön formulieren (algebraische 
Datentypen, strukturelle Zerlegung etc.), ergibt in den alten, 
imperativen Sprachen wie C eben if-else-if-else-Kaskaden. Diese Kaskaden 
sind aber meiner Meinung nach leicht zu warten und zu verstehen, weil 
sie die Fälle aufzählen und dem menschlichen Denken entsprechen.

Aber wie immer, das ist subjektiv, jeder hat da seine eigenen Prägungen.

von udok (Gast)


Lesenswert?

@Walter:
Warum nicht einfach die Betriebssystem Funktion verwenden?
Bist ja nicht der erste, der das Problem hat...
... und deine C Kenntnisse sind noch nicht so weit, das du
das einfach und effizient lösen kannst.

Die ganze Optimiererei (int_fast8_t???, Assembler anschauen???)
macht doch 0 Sinn, wenn das Klump nicht funktioniert.
Denke nur mal an all die Spezialfälle, die nicht in der Spec stehen, 
wenn
sich etwa ein Latin1 Char einschleicht...

von A. S. (Gast)


Lesenswert?

oder wie Oliver das meinte, ein switch mit fallthrough
1
uint32_t utf8decode(uint8_t bytes[]) {
2
uint32_t result=0;
3
4
   switch(*bytes & 0xf0)
5
   {
6
   case 0xf0: result |= *bytes++ & 0x07; result<<=6;
7
              result |= *bytes   & 0x3f;
8
   case 0xe0: result |= *bytes++ & 0x0f; result<<=6;
9
   case 0xc0:  
10
   case 0xd0: result |= *bytes++ & 0x3f; result<<=6;
11
   default:   result |= *bytes++ & 0x7f; 
12
   }
13
   return result;
14
}

Ist unwartbar, obwohl er "nach Schema F" aussieht.

von Walter T. (nicolas)


Lesenswert?

Yalu X. schrieb:
> Wie wär's mit einer Schleife statt der If-Kaskade?

Die ist es mittlerweile auch geworden.

Aber Respekt: Alle drei Varianten funktionieren.

von Peter D. (peda)


Lesenswert?

Walter T. schrieb:
> int_fast8_t r = __builtin_clz(~a);

Solche Obfuscationen macht man erst, wenn riesige Datenmengen in kurzer 
Zeit zu verarbeiten sind.
Laß das ganze _fast und _builtin Gedöns weg und schreib lesbaren und 
funktionierenden Code.

von Walter T. (nicolas)


Lesenswert?

Peter D. schrieb:
> Solche Obfuscationen macht man erst, wenn riesige Datenmengen in kurzer
> Zeit zu verarbeiten sind.

Was wäre denn Deine nicht-merkwürdige Variante, führende Einsen zu 
zählen? While-Schleife und hoffen, dass der Compiler das als Idiom 
versteht?

(Bei den fast-Int bin ich nicht Deiner Meinung. An die habe ich mich so 
gewöhnt, dass sie für mich mittlerweile "normaler" als "int" und "long" 
aussehen.)

: Bearbeitet durch User
von Rolf M. (rmagnus)


Lesenswert?

Peter D. schrieb:
> Laß das ganze _fast und _builtin Gedöns weg und schreib lesbaren und
> funktionierenden Code.

int_fast8_t ist ein Standard-typedef. Da ist nichts falsch dran.

von mh (Gast)


Lesenswert?

Rolf M. schrieb:
> Peter D. schrieb:
>> Laß das ganze _fast und _builtin Gedöns weg und schreib lesbaren und
>> funktionierenden Code.
>
> int_fast8_t ist ein Standard-typedef. Da ist nichts falsch dran.

Falsch nicht, aber der Nutzen ist sehr begrenzt und häufig ist es 
kontraproduktiv. Das Problem ist das "fast". Was genau bedeutet es? In 
welchen Situationen ist ein int_fast8_t schneller als ein int8_t? Gibt 
es Situationen wo ein int_fast8_t langsamer ist als ein int8_t?

Da die fast-Varianten nur typedefs sind, kann man dem Compiler damit 
schonmal ordentlich ausbremsen. Oder haben Compiler mittlerweile extra 
Logik für diese Typen integriert? Also weiß der Compiler, dass ein 
int_fast8_t einen 8Bit Wertebereich hat, auch wenn es ein typedef auf 
einen int32 ist?

von Prokrastinator (Gast)


Lesenswert?

Rolf M. schrieb:
> goto zu vermeiden, weil man halt mal
> indoktriniert wurde, dass das die Saat des Bösen sei.

Hihi, ich hatte mal einen Anruf des Programmierers eines Kunden, der 
völlig Entsetzt, mit bebender Stimme sagte: 'Du hast GOTO verwendet ...'

Ja. Stimmt. Der Kandidat hat 100 Punkte.
Ist Teil des C Spachstandards und war an der Stelle die übersichtlichste 
Methode aus dem Loop zu entkommen. Für Glaubensfragen bitte an den 
Esotheriker des geringsten Mißtrauens wenden.

Den Teil mit Inline ASM und den Jumps auf Sprungmarken fand er okay.
Ist ja ein mega Unterschied zu GOTO ...

Den Teil mit der Pointerarithmetik ohne Bereichsprüfung hat er nicht mal 
gecheckt. Ist mir dann noch rechtzeitig aufgefallen.

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

mh schrieb:
> In welchen Situationen ist ein int_fast8_t schneller als ein int8_t?

Kann dir beim ARM schnell passieren, denn da muss u.U. noch zusätzlich 
maskiert werden, während der int_fast8_t 32 Bit breit ist und damit die 
natürliche Maschinenwortbreite nutzen kann.

Packst du den gleichen Code aber auf einen AVR, kann der Compiler dafür 
tatsächlich eine 8-Bit-Einheit nehmen.

von mh (Gast)


Lesenswert?

Jörg W. schrieb:
> mh schrieb:
>> In welchen Situationen ist ein int_fast8_t schneller als ein int8_t?
>
> Kann dir beim ARM schnell passieren, denn da muss u.U. noch zusätzlich
> maskiert werden, während der int_fast8_t 32 Bit breit ist und damit die
> natürliche Maschinenwortbreite nutzen kann.
Mir ist klar, dass es Situationen gibt, in denen es Vorteile gibt. 
Deswegen enthält mein Betrag mehr als die eine Frage. Ist der Vorteil 
beim Maskieren  größer als der Nachteil beim Kopieren wo 3 unnötige 
Bytes angefasst werden müssen? Kann der Compiler nicht vielleicht auch 
für den int8_t das 32Bit Register benutzen? Verhindert der int_fast8_t 
alias int32 eine Optimierung weil der Compiler nicht mehr sehen kann, 
dass der echte Wertebereich nur 8Bit groß ist?

von Experte (Gast)


Lesenswert?

Peter D. schrieb:
> Walter T. schrieb:
>> int_fast8_t r = __builtin_clz(~a);
>
> Solche Obfuscationen macht man erst, wenn riesige Datenmengen in kurzer
> Zeit zu verarbeiten sind.

Witzig dabei auch, dass __builtin_clz(0) undefiniert ist...

von Walter T. (nicolas)


Lesenswert?

Experte schrieb:
> Witzig dabei auch, dass __builtin_clz(0) undefiniert ist...

Ein UTF-8-Zeichen, das mit 0xFF anfängt, aber auch.

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

mh schrieb:
> Kann der Compiler nicht vielleicht auch für den int8_t das 32Bit
> Register benutzen?

Ja, kann er nicht. int8_t sagt "nimm genau 8 Bit". int_fast8_t sagt 
"nimm mindestens 8 Bit, gern auch mehr, wenn es das schneller macht".

Der Vollständigkeit halber: int_least8_t sagt: "nimm möglichst 8 Bit; 
wenn du das nicht kannst, nimm halt mehr". Der Unterschied zu int8_t 
ist, dass eine Maschine, die keine 8-Bit-Einheiten handhaben kann (da 
gibt's wohl ein paar DSPs) halt zwar ein int_least8_t implementieren 
kann, aber ein int8_t kann sie dann schlicht nicht anbieten. Code, der 
so geschrieben ist, dass er per int8_t unbedingt genau 8 Bit haben 
möchte, lässt sich dann nicht compilieren.

von Klaus W. (mfgkw)


Lesenswert?

Yalu X. schrieb:
> Wie wär's mit einer Schleife statt der If-Kaskade?

Ah, die berühmte if-Schleife? :-)

von Walter T. (nicolas)


Lesenswert?

Interessanter finde ich aber trotzdem die Frage, was an den 
__builtin-Funktionen verkehrt ist und was die bessere Alternative ist.

Mein naiver Ansatz war ja: Sie sind schon da. Sie sind dokumentiert. 
Also kann man sie benutzen.

Wenn ich für den MSVC austauschbaren Code geschrieben hätte, hätte ich 
sie wohl in eine Funktion clz() gewrappt, wohl oder übel eine 
Compilerweiche benutzt und den anderen Zweig selbst implementiert. Mit 
dem Nebeneffekt, noch einmal einen eigenen Header mehr einfügen zu 
müssen.

: Bearbeitet durch User
von Markus F. (mfro)


Lesenswert?

Walter T. schrieb:
> Interessanter finde ich aber trotzdem die Frage, was an den
> __builtin-Funktionen verkehrt ist

Nix, wenn dich nicht stört, daß sie nicht portabel und (auch wenn Du 
immer und ewig "nur" gcc benutzt) nur in einer hosted-Umgebung zur 
Verfügung stehen.

von Walter T. (nicolas)


Lesenswert?

Markus F. schrieb:
> Nix, wenn dich nicht stört, daß sie nicht portabel und (auch wenn Du
> immer und ewig "nur" gcc benutzt) nur in einer hosted-Umgebung zur
> Verfügung stehen.

Kannst Du das näher erläutern? Ich verstehe den Begriff der 
"hosted-Umgebung" in diesem Zusammenhang nicht.

von Christian F. (christian_f476)


Lesenswert?

Klaus W. schrieb:
> berühmte if-Schleife? :-)

Kurz nach dem Umstieg von Pacsal:
1
#define repeat do
2
#define until(x) while(x)
3
4
  repeat
5
  {
6
    _delay_ms(500);
7
  }
8
  until(messwert_lesen() == 1);

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

Walter T. schrieb:
> Ich verstehe den Begriff der "hosted-Umgebung" in diesem Zusammenhang
> nicht.

-fhosted (default Option des Compilers)

Es wird eine Standardbibliothek vorausgesetzt, und der Compiler darf 
entsprechend der standardmäßig definierten Bedeutung auch 
Bibliotheksfunktionen optimieren. Beispielsweise kann er dabei 
strlen("hi") durch die Konstante 2 ersetzen. Andererseits sind in diesem 
Falle auch Prototypen für main() genormt, das muss entweder
1
int main(void);

oder
1
int main(int, char **);

sein. Das im Embedded-Bereich so gern geschriebene
1
void main(void);

verletzt theoretisch diese Regel (und gibt entsprechend eine Warnung).

Alternative: "freestanding environment", -ffreestanding

Hier definiert der Standard einen deutlich eingeschränkteren 
Funktionsumfang, es ist keine Standardbibliothek vorausgesetzt, main() 
darf man beliebig deklarieren (es darf auch ganz fehlen). Aber: es kann 
halt auch keine Standardbibliothek optimiert werden.

Daher arbeiten selbst embedded-Systeme sehr oft mit -fhosted.

von Markus F. (mfro)


Lesenswert?

Walter T. schrieb:
> Kannst Du das näher erläutern? Ich verstehe den Begriff der
> "hosted-Umgebung" in diesem Zusammenhang nicht.

Schau' mal hier: 
https://gcc.gnu.org/onlinedocs/gcc/C-Dialect-Options.html 
(-ffreestanding bzw. -fno-hosted).

von Walter T. (nicolas)


Lesenswert?

Jörg W. schrieb:
> -fhosted (default Option des Compilers)

Diese Erklärung ist sehr schön, und sollte irgendwo oben angepinnt sein. 
Hätte ich sie viel früher gelesen, hätte mir das viel Zeit gespart.

Aber ich finde nirgendwo ein Indiz dafür, dass die buildins in 
"freestanding" nicht verfügbar wären.

von Rolf M. (rmagnus)


Lesenswert?

mh schrieb:
> Rolf M. schrieb:
>> Peter D. schrieb:
>>> Laß das ganze _fast und _builtin Gedöns weg und schreib lesbaren und
>>> funktionierenden Code.
>>
>> int_fast8_t ist ein Standard-typedef. Da ist nichts falsch dran.
>
> Falsch nicht, aber der Nutzen ist sehr begrenzt und häufig ist es
> kontraproduktiv.

Ja? Wann denn?

> Das Problem ist das "fast". Was genau bedeutet es? In welchen Situationen
> ist ein int_fast8_t schneller als ein int8_t?

Eine allgemengültige Antwort gibt es dafür nicht. Aber bei manchen 
Architekturen sind die Datentypen tendenziell unterschiedlich effizient 
im Bezug auf Rechenzeit. Und der kleinste muss nicht gezwungenermaßen 
auch der schnellste sein.

> Gibt es Situationen wo ein int_fast8_t langsamer ist als ein int8_t?

Kann es auch geben.

> Da die fast-Varianten nur typedefs sind, kann man dem Compiler damit
> schonmal ordentlich ausbremsen.

Das gilt für int8_t genauso.

> Oder haben Compiler mittlerweile extra Logik für diese Typen integriert?

gcc hat das anscheinend aktuell nicht, soweit ich erkennen kann.

mh schrieb:
> Mir ist klar, dass es Situationen gibt, in denen es Vorteile gibt.
> Deswegen enthält mein Betrag mehr als die eine Frage. Ist der Vorteil
> beim Maskieren  größer als der Nachteil beim Kopieren wo 3 unnötige
> Bytes angefasst werden müssen?

Werden sie sowieso. Das Prozessor-Register ist so oder so 32 Bit breit, 
und der Speicher wird oft auch sowieso immer in 32-Bit-Einheiten 
angesprochen. Der Unterschied ist dann nur, dass bei einem Kopieren 
eines 8-Bit-Werts aus dem Speicher die anderen 32 Bytes weggeworfen 
werden, bzw. beim Schreiben gar noch umständlich gelesen, dann das eine 
Byte ausgestauscht, dann wieder zurückgeschrieben werden müssen.

> Kann der Compiler nicht vielleicht auch für den int8_t das 32Bit Register
> benutzen?

Das muss er sowieso, weil der ARM gar keine 8-Bit-Register hat. Wenn du 
aber auf 8 Bit festzurrst, muss er sicherstellen, dass da nachher der 
gleiche Wert drin steht, als wäre das Register 8 Bit breit gewesen. Und 
das kann zusätzlichen Aufwand bedeuten.

> Verhindert der int_fast8_t alias int32 eine Optimierung weil der Compiler
> nicht mehr sehen kann, dass der echte Wertebereich nur 8Bit groß ist?

Nein.

von Walter T. (nicolas)


Lesenswert?

Interessant. Das Schweigen der Experten sagt mehr als viele Worte 
anderer.

Zu "__builtin -- lieber nicht" gibt es von denjenigen in diesem Thread, 
von deren Kompetenz ich überzeugt bin, weder Zustimmung und Begründung 
noch Ablehnung.

Es scheint also ein vages Unwohlsein zu erzeugen, auf dessen Quelle man 
nicht genau mit dem Finger zeigen kann.

von A. S. (Gast)


Lesenswert?

Walter T. schrieb:
> Interessanter finde ich aber trotzdem die Frage, was an den
> __builtin-Funktionen verkehrt ist und was die bessere Alternative ist.
>
> Mein naiver Ansatz war ja: Sie sind schon da. Sie sind dokumentiert.
> Also kann man sie benutzen.

Du kannst sie ja auch benutzen. Aber erst am Ende, wenn Messungen einen 
Bottleneck ergeben und es für diese Plattform wichtig ist. .

Im Ursprungscode sieht es hingegen fast so aus, als seien die Funktionen 
um das clz herum gebaut worden. Natürlich ist keines der Codebeispiele 
hier wirklich sinnvoll. Doch fällt gerade bei solchen Aufgaben der Code 
am Ende in sich zusammen, wenn man ihn portabel formuliert und 
verbessert. Nicht, wenn man ihn um eine fancy Funktion herum baut.

von Walter T. (nicolas)


Lesenswert?

A. S. schrieb:
> Im Ursprungscode sieht es hingegen fast so aus, als seien die Funktionen
> um das clz herum gebaut worden.

Wenn der einschlägige Teil des Standards, den man vorliegen hat, 
aussagt: "Die Anzahl der Einsen/die Stellung der ersten Null am Anfang 
des ersten Bytes bestimmt, was dann gemacht wird" (Wikipedia).
Mein erster Gedanke: "OK, ich muss Einsen zählen. Moment mal. Für den 
Zweierlogarithmus hatte ich eine nette Funktion, die Nullen zählt, 
also..."

Wie würdest Du anfangen? Was machst Du ohne Bottleneck?


Das Fragen mag penetrant sein. Aber ich will verstehen, was für einen 
Softwareentwickler "normal" ist. Im Internet findet man wenig 
"normales". Man findet hauptsächlich laute Minderheiten. Ich meine: Wenn 
wir unsere sexuelle Früherziehung im Internet genossen und uns daran 
orientiert hätten, was "normal" ist...

Ich habe im Alltag keinen Kontakt zu Softwareentwicklern.

: Bearbeitet durch User
von Vincent H. (vinci)


Lesenswert?

Walter T. schrieb:
> Interessanter finde ich aber trotzdem die Frage, was an den
> __builtin-Funktionen verkehrt ist und was die bessere Alternative ist.

In C++20 gibts std::countl_zero
https://en.cppreference.com/w/cpp/numeric/countl_zero

von udok (Gast)


Lesenswert?

Walter T. schrieb:
> Wenn der einschlägige Teil des Standards, den man vorliegen hat,
> aussagt: "Die Anzahl der Einsen/die Stellung der ersten Null am Anfang
> des ersten Bytes bestimmt, was dann gemacht wird" (Wikipedia).

Das sagt der Standard eben nicht, bzw nur mit Einschränkungen.
Ohne sich mit dem Unicode Standard auseinanderzusetzen, kommen
da nur halbgare Sachen raus, allzu oft auch Sicherheitslücken
(Stichwort Überlange UTF-8 Sequenzen, Codepoint <= 0x10ffff,
Ungültige Zeichenbereiche, CESU-8).  Abgesehen vom Standard hast du
noch das Problem, dass es auch reichlich ungültiges UTF-8 gibt,
mit dem man aber in der Praxis umgehen muss.

Aber was willst du denn eigentlich machen?
Das ist doch erst mal die entscheidende Frage, die du nicht
beantwortest (so wie 90% der TE hier).
Ich habe so den Eindruck, dass du einfach jemanden zum Quatschen
brauchst, auch ok.
In der Zeit, die du hier verplemperst, hättest du dir auch
einige UTF-8 Libs auf github anschauen können, oder
in der Doku deiner uns unbekannten Entwicklungsumgebung suchen
können.

Optimieren ohne Ziel, und ohne Performancemessung ist doch reine 
Zeitverschwendung.  Wahrscheinlich liest deine Anwendung
gerade mal 100 Config Bytes, und das geht halt dann vielleicht
in 20 us statt in 30 us.
Aber schon dein Ansatz, einzelne Zeichen anzuschauen verhindert es,
das da irgendwas mit guter Performance rauskommt.
Du musst blockweise arbeiten, wenn es schnell gehen soll.
Ob du int8_t oder int verwendest, geht da im Rauschen unter,
aber alles kleiner als int wird in C erst mal auf int konvertiert.

Der Profi widmet dem Testen und der Behandlung von Fehlerzuständen
eher mehr Zeit, als der Implementierung der Grundfunktion.
Dann kommt erst die Optimierung und auch nur wenn es notwendig ist,
oder wenn Zeit ist. Bei dir ist es umgekehrt.

Als Referenz: die ziemlich gut optimierte Win32 MultiByteToWideChar 
Funktion schafft 1.2 Zeichen / Takt für Ascii Zeichen,
und ca. 0.5 Zeichen / Takt für komlexere Zeichen (ohne SSE 
Erweiterungen).
Eine moderne SSE Funktion schafft über 4 Zeichen / Takt.

Aber brauchst du wirklich UTF-8?  Am Mikrocontroller?
Nimm doch einfach ASCII oder Latin1, und gut ist es.
Schau mal, das deine Anwendung läuft, und wenn du dann Chinesische
Zeichen brauchst, ist UTf-8 vielleicht gar nicht die beste Wahl.

von Walter T. (nicolas)


Lesenswert?

udok schrieb:
> Optimieren ohne Ziel, und ohne Performancemessung ist doch reine
> Zeitverschwendung.

Dazu gerne in einem anderen Thread, den ich soeben eröffnet habe.

udok schrieb:
> Aber was willst du denn eigentlich machen?

Ich dachte, das sei durch die umfangreiche Kommentierung des Quelltextes 
im Eröffnungsbeitrag geklärt. Ich brauchte einen UTF-Dekoder. Performanz 
ist auch nicht ganz nebensächlich, weil da wirklich jede einzelne 
Zeichenkette durchgeht, die auf dem Display erscheint. Mit chinesischen 
Zeichen habe ich es nicht. Aber mit dem griechischen Alphabet und ein 
paar anderen Formelzeichen.

Aber das läuft ja alles seit Samstag.

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

Walter T. schrieb:

> Aber ich finde nirgendwo ein Indiz dafür, dass die buildins in
> "freestanding" nicht verfügbar wären.

Ich vermute, dass sie da auch verfügbar sind, aber wie heißt es so 
schön? "If all else fails, read the documentation."

von Walter T. (nicolas)


Lesenswert?

Die Doku schweigt sich dazu aus. Ich nehme an, es handelt sich um ein 
beredtes Schweigen.


Aber welcher Frage ich immer noch kein Stück näher gekommen bin: Warum 
wird hier sofort "premature optimization" geschriehen, sobald ein paar 
Unterstriche vorkommen?

: Bearbeitet durch User
von A. S. (Gast)


Lesenswert?

Walter T. schrieb:
> Mein erster Gedanke: "OK, ich muss Einsen zählen. Moment mal. Für den
> Zweierlogarithmus hatte ich eine nette Funktion, die Nullen zählt, also.

Vorzeitige Optimierung ist übel.

Solange Du die Aufgabe nicht durchdrungen hast, kann eine Optimierung 
keinen Sinn machen.

Das ist so, als würde man ein Schema suchen, obwohl man erst 5 der 8 
Sonderfälle gelesen hat. Wertlos, wenn man alle 8 implementieren will, 
weil es auch einfacher werden kann.

Wenn es nur 5 pattern gibt, ist Plain Code kein schlechter start, also 5 
ifs hier z.b.

von Walter T. (nicolas)


Lesenswert?

Okay, also für Dich ist
1
if( (byte0 & 0xC0) == 0xB0 )
2
{
3
  ...
4
} 
5
if( (byte0 & 0xF0) == 0xC0 )
6
{
7
  ...
8
}
also leichter zu lesen als
1
    uint32_t a = byte0;
2
    a <<= 24;
3
    int_fast8_t r = __builtin_clz(~a);

Das klingt für mich ungewohnt, aber vielleicht macht es die tägliche 
Vertrautheit mit HEX-Wüsten, die mir absolut fehlt.

: Bearbeitet durch User
von A. S. (Gast)


Lesenswert?

Walter T. schrieb:
> also leichter zu lesen als

Die beiden Schnipsel haben miteinander nichts zu tun. Ob Du  hexzahlen 
lesen kannst oder bitmuster (xxx_____ oder b11100000) nutzt, ist eine 
Sache.

Ob Du die Bestimmung der Einsen rausziehst und dafür eine (gedankliche) 
Übersetzung mehr hast, das andere.

Die wichtigen Fragen sind doch ganz andere: welchen Umfang willst Du 
dekodieren? Wie gehst Du mit Zeichen außerhalb um, wie mit fehlerhaften 
oder abgebrochenen (passiert sehr schnell mit strncpy + 0). Was ist als 
Funktion(sset) gut nutzbar (deine erfordert drumherum nochmal den 
gleichen Overhead)

Bis dahin habe ich nichts dagegen, dass auch zu programmieren (um es zu 
testen, zu verstehen) aber nicht Tricky in Sütterlin auf Stein 
gemeißelt, sondern lesbar und einfach.

von Peter D. (peda)


Lesenswert?

Walter T. schrieb:
> Was wäre denn Deine nicht-merkwürdige Variante, führende Einsen zu
> zählen? While-Schleife und hoffen, dass der Compiler das als Idiom
> versteht?

Nun, erstmal kann nicht jede Maschine ein __builtin_clz und 2. mußte ich 
erstmal nachsehen, was das überhaupt macht. Und 3. ist Dein Code sowas 
von hinten durch die Brust ins Auge, nur um es benutzen zu können. Ich 
hab da ewig gebraucht, um durchzusehen.

Ich würde solche einfachen Sachen direkt hinschreiben, ohne komplizierte 
Konstrukte:
1
/** UTF8-Zeichenlaenge anhand des ersten Bytes ermitteln
2
 * Die Laenge eines UTF-8-Zeichens (1 bis 4 Byte) ist im ersten Zeichen einkodiert.
3
 *       0xxxxxxx -> 1 Byte
4
 *       110xxxxx -> 2 Bytes
5
 *       1110xxxx -> 3 Bytes
6
 *       11110xxx -> 4 Bytes
7
 * @return Anzahl der Bytes des UTF-8-Zeichens */
8
9
int_fast8_t utf8bytes(const char *s0)
10
{
11
  switch (*s0 & 0xF0)
12
  {
13
    case 0xF0: return 4;
14
    case 0xE0: return 3;
15
    case 0xC0:
16
    case 0xD0: return 2;
17
    default:   return 1;
18
  }
19
}

von Walter T. (nicolas)


Lesenswert?

Peter D. schrieb:
>     case 0xC0:
>     case 0xD0: return 2;

Okay, wir haben wirklich sehr unterschiedliche Denkweisen, was als 
"naheliegend" zu bezeichnen ist.

Insofern macht das die Diskussion über den ursprünglichen Anwendungsfall 
hinaus interessant.

: Bearbeitet durch User
von A. S. (Gast)


Lesenswert?

Peter D. schrieb:
> Ich würde solche einfachen Sachen direkt hinschreiben, ohne komplizierte
> Konstrukte:

Ich hab das oben ja genauso gemacht, nur ist das halt Tricky und nicht 
wirklich das gewünschte. Du kannst 4 nicht von 5 einen unterscheiden und 
bei Default fallen auch alle Fehler rein.

von Experte (Gast)


Lesenswert?

Walter T. schrieb:
> Okay, wir haben wirklich sehr unterschiedliche Denkweisen, was als
> "naheliegend" zu bezeichnen ist.

Naja, wer das hier schreibt:
1
  switch(1)
2
  {
3
    case 1:

beurteilt "andere" Denkweisen?

Kennst Du den uralten Witz mit Bart:

> Verkehrsfunk: "Achtung: Ein Falschfahrer auf der Autobahn!"
> Autofahrer: "Ein Falschfahrer? Hunderte!"

von Walter T. (nicolas)


Lesenswert?

Der Teil mit dem Heraussprung war ja auch der Teil, mit dem ich von 
Anfang an unzufrieden war - Anlass für diesen Thread. Der Teil mit den 
Einsen zählen normal. Bitte nicht mischen!

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.