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_tx=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.
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.
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.
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.
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.
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...
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.
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?
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?
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.
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.
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.
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:
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.
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")...
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..."
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:
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?