Forum: Compiler & IDEs uint32_t variable byteweise füllen


von Micha (Gast)


Lesenswert?

ich hab gerade folgendes Problemchen:

eine funktion uint8_t nextByte() liefert bei jedem Aufruf das nächste 
Byte eines Datenstroms. Vier solche Byte sollen in eine uint32_t 
Variable einsortiert werden, wobei das niedrigstwertige Byte zuerst 
kommt. Der folgende Konstrukt macht nicht was er soll:
1
 
2
uint32_t x = 0;
3
  for (i=0;i<4;i++) x += nextByte() << (8*i);
Kriegt man das mit einem Cast hin? Bräuchte da mal einen Anschubser wie 
die Sytax ausschaut.

von Andreas R. (andreasr)


Lesenswert?

1
uint32_t x = 0;
2
for (i = 0; i < 4; i++)
3
{
4
    x >>= 8;
5
    x |= ((uint32_t)nextByte()) << 24;
6
}

von Eric B. (beric)


Lesenswert?

Micha schrieb:
> ich hab gerade folgendes Problemchen:
>
> eine funktion uint8_t nextByte() liefert bei jedem Aufruf das nächste
> Byte eines Datenstroms. Vier solche Byte sollen in eine uint32_t
> Variable einsortiert werden, wobei das niedrigstwertige Byte zuerst
> kommt.
1
uint32_t x = 0;
2
3
// LSB first
4
for(i = 0; i < 4; i++) {
5
  x >>= 8;
6
  x += (uint32_t) nextByte() << 24;
7
}
8
9
// MSB first
10
for(i = 0; i < 4; i++) {
11
  x <<= 8;
12
  x += (uint32_t) nextByte();
13
}

EDIT: Heh, Andreas war schneller :-)

: Bearbeitet durch User
von Karl H. (kbuchegg)


Lesenswert?

Micha schrieb:

> kommt. Der folgende Konstrukt macht nicht was er soll:

> x += nextByte() << (8*i);

Das dein x ein uint32_t ist, interessiert den Compiler nicht, wenn er 
das Ergebnis von nextByte um eine gewisse Anzahl Stellen nach links 
verschiebt.

Da nextByte() wohl einen uin8_t liefern wird, ist das daher eine int 
Rechnung, d.h. 16 Bit.
1
    x += (uint32_t)nextByte() << (8*i);

Du musst dir eines merken.
Wenn du eine Berechnung dergestalt hast
1
  ergebnis = Ausdruck;

dann ist der Datentyp von 'ergebnis' völlig uninteressant, wenn es darum 
geht zu bestimmen, mit welchen Operationen (Bitlänge) 'Ausdruck' 
ausgewertet wird. Bei Operationen wie + - * / sind einzig und alleine 
die Datentypen der beteiligten Operanden
1
   ergebnis = a * b;
a bzw. b ausschlaggebend, ob das eine 16 Bit oder eine 32 Bit 
Multiplikation ist. Erst das Ergebnis des kompletten Ausdrucks wird dann 
ganz zum Schluss an den Datentyp angepasst, so dass er an 'ergebnis' 
zuweisbar ist. Bei Schiebeoperationen
1
   ergebnis = a << 4;
ist dann sogar lediglich der Datentyp von a dafür massgeblich, ob da 16 
Bit oder 32 Bit geschoben wird und das Ergebnis daher dann den 
entsprechenden Datentyp hat.

> Bräuchte da mal einen Anschubser
Was du brauchst, ist das Durcharbeiten eines C-Buchs.

: Bearbeitet durch User
von Andreas R. (andreasr)


Lesenswert?

> EDIT: Heh, Andreas war schneller :-)

Aber im Prinzip der gleiche Code ;-)

von Pointeruser (Gast)


Lesenswert?

1
#include <stdio.h>
2
3
uint8_t   byteval[4] = {0x11, 0x22, 0x33, 0x44};
4
int16_t   ii;
5
uint32_t  word32;
6
7
int main (void)
8
{
9
  word32 = 0;
10
  for (ii=0; ii<4; ii++)
11
  {
12
    *(((uint8_t*)&word32)+ii) = byteval[ii];
13
  }
14
}

von Pit (Gast)


Lesenswert?

Pointeruser schrieb:
> #include <stdio.h>
>
> uint8_t   byteval[4] = {0x11, 0x22, 0x33, 0x44};
> int16_t   ii;
> uint32_t  word32;
>
> int main (void)
> {
>   word32 = 0;
>   for (ii=0; ii<4; ii++)
>   {
>     *(((uint8_t*)&word32)+ii) = byteval[ii];
>   }
> }

Schönes Beispiel, dass man Pointer nur einsetzen soll, wenn man ihn auch 
wirklich benötigt.

von Dr. Sommer (Gast)


Lesenswert?

Pointeruser schrieb:
> #include <stdio.h>
>
> uint8_t   byteval[4] = {0x11, 0x22, 0x33, 0x44};
> int16_t   ii;
> uint32_t  word32;
>
> int main (void)
> {
>   word32 = 0;
>   for (ii=0; ii<4; ii++)
>   {
>     *(((uint8_t*)&word32)+ii) = byteval[ii];
>   }
> }

Hier wie üblich das Problem, dass C(++) nicht definiert was das Ergebnis 
ist; word32 kann danach zB 0x11223344 oder auch 0x44332211 enthalten.
Warum eine so temporäre Variable wie "ii" global sein muss ist auch 
nicht ganz klar.
Die zuerst geposteten Antworten von Eric bzw. Andreas sind wesentlich 
besser, weil hier das Ergebnis klar ist.

von Micha (Gast)


Lesenswert?

Vielen Dank für alle Antworten!
Ja, ich sollte mir wohl vom Osterhasen ein C Buch wünschen - falls nicht 
schon zu spät ;-)

Das mit der Pointerei war mir auch schon durch den Kopf gegangen. Das 
hat was, aber da kommt wie angedeutet zusätzlich die Frage ins Spiel, ob 
die uint32_t Variable nun auf dem konkreten System little oder big 
endian ist. Hab grade gegoogelt - beim 8-Bit AVR müsste es so passen, 
der ist little endian...

von Pointeruser (Gast)


Lesenswert?

Dr. Sommer schrieb:
> Hier wie üblich das Problem, dass C(++) nicht definiert was das Ergebnis
> ist; word32 kann danach zB 0x11223344 oder auch 0x44332211 enthalten.

Ich denke wir reden hier über AVR code, das sollte die
Endianess klar sein.

Dr. Sommer schrieb:
> Warum eine so temporäre Variable wie "ii" global sein muss ist auch
> nicht ganz klar.

Wer hier Korinthen kacken will darf das.

von Dr. Sommer (Gast)


Lesenswert?

Ich verstehe nicht, warum die schlechte Lösung mit Pointern oder gar 
unions so populär ist. Man könnte sich alle Gedanken über 
Little/Big-Endian und Portierbarkeit sparen wenn man einfach Bitshifts 
(wie Eric und Andreas) verwendet. Warum würdem an auf die Idee kommen so 
etwas mit Pointer umcasten zu machen?

von Pointeruser (Gast)


Lesenswert?

Hallo Korinthenkacker,

wer hat hier was von Portierbarkeit gewünscht oder gesprochen?

Welche Eigenschaften muss ein C-Sourcecode haben dass er
"schlecht" oder "gut" ist?

von Dr. Sommer (Gast)


Lesenswert?

Hallo Pointerfrickler,

du bist natürlich frei beliebig schlechten Code zu schreiben. Allerdings 
sollte man als "guter" Programmierer es sich angewöhnen, grundsätzlich 
korrekten und portierbaren Code zu schreiben, denn dann macht man es 
automatisch richtig wenn es drauf ankommt. Das gilt insbesondere für 
Fälle, in denen die korrekte Lösung in jeder Hinsicht besser ist und 
noch nicht einmal länger oder komplizierter.
Du kannst natürlich auch grundsätzlich Auto fahren ohne dich 
anzuschnallen, und wenn du nur in 10km/h-Zonen fährst macht das 
vielleicht auch nichts, aber es ist dennoch allgemein eine gute Idee 
sich sicher zu gehen. Vielleicht fährt man ja doch mal auf der Autobahn.

Es gibt viele Kriterien für guten oder schlechten Code. Ein Kriterium 
für guten Code ist auf jeden Fall dass er korrekt ist und der Standard 
ein definiertes Ergebnis garantiert.

von Eric B. (beric)


Lesenswert?

Dr. Sommer schrieb:
> Ich verstehe nicht, warum die schlechte Lösung mit Pointern oder gar
> unions so populär ist. Man könnte sich alle Gedanken über
> Little/Big-Endian und Portierbarkeit sparen wenn man einfach Bitshifts
> (wie Eric und Andreas) verwendet. Warum würde man auf die Idee kommen so
> etwas mit Pointer umcasten zu machen?

Weil man sich dann eben die Bitshifterei sparen kann. Auf einem 8-bit 
Architektur wo es nur ein OpCode gibt um ein Byte um ein Bit nach links 
oder rechts zu schieben, kann das dann schon "lange" dauern.

von Peter II (Gast)


Lesenswert?

Eric B. schrieb:
> Weil man sich dann eben die Bitshifterei sparen kann. Auf einem 8-bit
> Architektur wo es nur ein OpCode gibt um ein Byte um ein Bit nach links
> oder rechts zu schieben, kann das dann schon "lange" dauern.

der Compiler ist aber auch nicht so dumm und schiftet um 8bit.

von Eric B. (beric)


Lesenswert?

Peter II schrieb:
> Eric B. schrieb:
>> Weil man sich dann eben die Bitshifterei sparen kann. Auf einem 8-bit
>> Architektur wo es nur ein OpCode gibt um ein Byte um ein Bit nach links
>> oder rechts zu schieben, kann das dann schon "lange" dauern.
>
> der Compiler ist aber auch nicht so dumm und schiftet um 8bit.

:-) Und rollt auch sofort die for-Schleife aus:
1
  x  = (uint32) readByte();
2
  x |= (uint32) readByte() << 8;
3
  x |= (uint32) readByte() << 16;
4
  x |= (uint32) readByte() << 24;

von Micha (Gast)


Lesenswert?

Peter II schrieb:
> der Compiler ist aber auch nicht so dumm und schiftet um 8bit.
der Compiler shiftet an der Stelle eher gar nichts, würde ich sagen

von Frank M. (ukw) (Moderator) Benutzerseite


Lesenswert?

Pointeruser schrieb:
> wer hat hier was von Portierbarkeit gewünscht oder gesprochen?

Du wirst auch noch im Laufe der Jahre lernen, dass ein gut portierbarer 
Code sehr wichtig ist. Warum? Die Prozessoren entwickeln sich weiter und 
es kommen laufend neue hinzu. Auch Du wirst in 30 Jahren keine ATTinys 
mehr programmieren - einfach, weil es sie nicht mehr gibt und längst 
durch andere Generationen ersetzt worden sind.

Deine Programme aber können solche Prozessor-Sprünge weitestgehend 
überleben - nämlich dann, wenn Du JETZT schon mitdenkst.

Nichts ist schlimmer als solche Stellen wie oben in einem größeren 
Projekt allesamt nachflicken zu müssen, weil derjenige, der den Code 
verzapft hat, so kleingeistig war, dass er Blicke über den Tellerrand 
nie zu wagen gedachte.

> Welche Eigenschaften muss ein C-Sourcecode haben dass er
> "schlecht" oder "gut" ist?

Portabler Code: Gut
Lesbarer Code: Gut

Unlesbarer Code: Schlecht
Unportabler Code: Ganz schlecht

So einfach ist das.

von WS (Gast)


Lesenswert?

typedef union {
    char  c[4];
    short i16[2];
    long  i32;
} ltyp;


etc.

WS

von Dr. Sommer (Gast)


Lesenswert?

WS schrieb:
> typedef union {
>     char  c[4];
>     short i16[2];
>     long  i32;
> } ltyp;
>
> etc.
>
> WS
Auch hier gilt das gleiche wie für die Pointer-Version: Das Ergebnis ist 
undefiniert. Noch schlimmer, es wurden die Typen char, short, long 
verwendet, und die müssen keineswegs 8/16/32 Bit-Typen entsprechen; long 
kann zB 64bit groß sein...

Micha schrieb:
> Peter II schrieb:
>> der Compiler ist aber auch nicht so dumm und schiftet um 8bit.
> der Compiler shiftet an der Stelle eher gar nichts, würde ich sagen
Es war natürlich der erzeugte Code gemeint. Und ja, Compiler optimieren 
die Bitshifts natürlich weg, sodass der erzeugte Code mindestens so 
effizient ist wie die Pointer-Version. Wahrscheinlich sogar noch 
effizienter, da er so komplett mit Registern arbeiten kann, während die 
Pointer-Version ein Ablegen im Speicher erfordert (falls der Compiler 
das nicht auch "sieht")...

von Pointeruser (Gast)


Lesenswert?

Dr. Sommer schrieb:
> Und ja, Compiler optimieren
> die Bitshifts natürlich weg, sodass der erzeugte Code mindestens so
> effizient ist wie die Pointer-Version.

Leider kann ein Compiler der 8-Bit Code erzeugen muss, Shift-
Operationen nicht über Byte-Grenzen hinweg optimieren. Somoit
zerfällt eine Shift-Operation einer 32 Bit Variablen zwangsläufig
in 3 bis 4 Stufen.

Was Zufolge hat dass die Version mit Pointern in jeder Optimierungs-
stufe weniger Code erzeugt und damit auch schneller ist als die
Version mit Shift-Operationen. Ich bin keiner der hier den
Geschwindigkeits-Wahn hat, aber viele bringen in so einer Diskussion
dieses Argument wenn es um den "besseren" Code geht.

Wer es nicht glauben will der kann es ja selbst am HeimStudio
ausprobieren / nachvollziehen.

Soviel zu Korinthenkackers Aussage "mindestens so effizient wie..."

von Dr. Sommer (Gast)


Lesenswert?

Der herr Pointerfrickler hat sogar teilweise Recht, der AVR-GCC ist zu 
blöd die 32bit-Shiftoperationen korrekt zu optimieren. Da die 
Pointer-Cast-Operation aber Stack-Zugriff erfordert, ist sie tatsächlich 
noch langsamer und größer. Hier der Testcode:
1
uint32_t foo () {
2
  uint8_t bytes [4] = { nextByte (), nextByte (), nextByte (), nextByte () };
3
  
4
  return  ((((uint32_t) bytes [0]) & 0xFF))
5
    |  ((((uint32_t) bytes [1]) & 0xFF) << 8)
6
    |  ((((uint32_t) bytes [2]) & 0xFF) << 16)
7
    |  ((((uint32_t) bytes [3]) & 0xFF) << 24);
8
}
9
10
uint32_t bar () {
11
  uint32_t x = 0;
12
  ((uint8_t*) &x) [0] = nextByte ();
13
  ((uint8_t*) &x) [1] = nextByte ();
14
  ((uint8_t*) &x) [2] = nextByte ();
15
  ((uint8_t*) &x) [3] = nextByte ();
16
  return x;
17
}
auf dem ATmega88 braucht die erste Version 39 Takte, und die Zweite 60. 
Außerdem ist die erste Version 6 Bytes kleiner.

Hier der Assemblercode mit drangeschriebenen Taktzahlen:
1
0000004c <foo>:
2
  4c:  1f 93         push  r17                  2
3
  4e:  cf 93         push  r28                  2
4
  50:  df 93         push  r29                  2
5
  52:  f9 df         rcall  .-14       ; 0x46 <nextByte>    3
6
  54:  c8 2f         mov  r28, r24                1
7
  56:  f7 df         rcall  .-18       ; 0x46 <nextByte>    3
8
  58:  d8 2f         mov  r29, r24                1
9
  5a:  f5 df         rcall  .-22       ; 0x46 <nextByte>    3
10
  5c:  18 2f         mov  r17, r24                1
11
  5e:  f3 df         rcall  .-26       ; 0x46 <nextByte>    3
12
  60:  41 2f         mov  r20, r17                1
13
  62:  50 e0         ldi  r21, 0x00  ; 0              1
14
  64:  60 e0         ldi  r22, 0x00  ; 0              1
15
  66:  70 e0         ldi  r23, 0x00  ; 0              1
16
  68:  ba 01         movw  r22, r20              1
17
  6a:  55 27         eor  r21, r21                1
18
  6c:  44 27         eor  r20, r20                1
19
  6e:  5d 2b         or  r21, r29                1
20
  70:  4c 2b         or  r20, r28                1
21
  72:  78 2b         or  r23, r24                1
22
  74:  cb 01         movw  r24, r22              1
23
  76:  ba 01         movw  r22, r20              1
24
  78:  df 91         pop  r29                    2
25
  7a:  cf 91         pop  r28                    2
26
  7c:  1f 91         pop  r17                    2
27
  7e:  08 95         ret
1
00000080 <bar>:
2
  80:  cf 93         push  r28                    2
3
  82:  df 93         push  r29                    2
4
  84:  00 d0         rcall  .+0        ; 0x86 <bar+0x6>      3
5
  86:  00 d0         rcall  .+0        ; 0x88 <bar+0x8>      3
6
  88:  cd b7         in  r28, 0x3d  ; 61              1
7
  8a:  de b7         in  r29, 0x3e  ; 62              1
8
  8c:  19 82         std  Y+1, r1  ; 0x01                2
9
  8e:  1a 82         std  Y+2, r1  ; 0x02                2
10
  90:  1b 82         std  Y+3, r1  ; 0x03                2
11
  92:  1c 82         std  Y+4, r1  ; 0x04                2
12
  94:  d8 df         rcall  .-80       ; 0x46 <nextByte>      3
13
  96:  89 83         std  Y+1, r24  ; 0x01              2
14
  98:  d6 df         rcall  .-84       ; 0x46 <nextByte>      3
15
  9a:  8a 83         std  Y+2, r24  ; 0x02              2
16
  9c:  d4 df         rcall  .-88       ; 0x46 <nextByte>      3
17
  9e:  8b 83         std  Y+3, r24  ; 0x03              2
18
  a0:  d2 df         rcall  .-92       ; 0x46 <nextByte>      3
19
  a2:  8c 83         std  Y+4, r24  ; 0x04              2
20
  a4:  69 81         ldd  r22, Y+1  ; 0x01              2
21
  a6:  7a 81         ldd  r23, Y+2  ; 0x02              2
22
  a8:  8b 81         ldd  r24, Y+3  ; 0x03              2
23
  aa:  9c 81         ldd  r25, Y+4  ; 0x04              2
24
  ac:  0f 90         pop  r0                      2
25
  ae:  0f 90         pop  r0                      2
26
  b0:  0f 90         pop  r0                      2
27
  b2:  0f 90         pop  r0                      2
28
  b4:  df 91         pop  r29                      2
29
  b6:  cf 91         pop  r28                      2
30
  b8:  08 95         ret

Just for Fun hier der selbe Code für ARMv7M kompiliert:
1
00000000 <foo>:
2
   0:  b570        push  {r4, r5, r6, lr}      5
3
   2:  f7ff fffe   bl  0 <nextByte>          1
4
   6:  4605        mov  r5, r0              1
5
   8:  f7ff fffe   bl  0 <nextByte>          1
6
   c:  4606        mov  r6, r0              1
7
   e:  f7ff fffe   bl  0 <nextByte>          1
8
  12:  4604        mov  r4, r0              1
9
  14:  0424        lsls  r4, r4, #16          1
10
  16:  f7ff fffe   bl  0 <nextByte>          1
11
  1a:  ea44 2406   orr.w  r4, r4, r6, lsl #8      1
12
  1e:  432c        orrs  r4, r5            1
13
  20:  ea44 6000   orr.w  r0, r4, r0, lsl #24      1
14
  24:  bd70        pop  {r4, r5, r6, pc}        5
15
  26:  bf00        nop
16
21
1
00000028 <bar>:
2
  28:  b500        push  {lr}            2
3
  2a:  b083        sub  sp, #12              1
4
  2c:  2300        movs  r3, #0            1
5
  2e:  9301        str  r3, [sp, #4]          2
6
  30:  f7ff fffe   bl  0 <nextByte>          1
7
  34:  f88d 0004   strb.w  r0, [sp, #4]        2
8
  38:  f7ff fffe   bl  0 <nextByte>          1
9
  3c:  f88d 0005   strb.w  r0, [sp, #5]        2
10
  40:  f7ff fffe   bl  0 <nextByte>          1
11
  44:  f88d 0006   strb.w  r0, [sp, #6]        2
12
  48:  f7ff fffe   bl  0 <nextByte>          1
13
  4c:  f88d 0007   strb.w  r0, [sp, #7]        2
14
  50:  9801        ldr  r0, [sp, #4]          2
15
  52:  b003        add  sp, #12              1
16
  54:  f85d fb04   ldr.w  pc, [sp], #4        2

Hier braucht die 1. Version 21 Takte und die 2. braucht 23 (bei den 
Pipeline-relevanten Instruktionen jeweils Best Case angenommen - im 
Worst case ist die Pointer-Version noch schlechter). Die 2. Version ist 
8 Bytes größer.

Soviel zu Pointerfricklers Aussage, dass die Pointer-Version irgendeinen 
Vorteil hätte. Vielleicht doch mal die Aussagen mit Fakten belegen?

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.