Hallo, ich habe neulich eine Beobachtung gemacht, die vielleicht den einen oder anderen interessieren könnte. also, auf Cortex-M sind 32bit-Operationen schneller als 8bit, weil die Maskierung entfällt. Das gilt auch beim Laden aus dem Speicher. Etwa lokale Zähler und Schleifenvariablen sind daher als 32bit-Integer schneller als in 8bit, auch wenn man nur den 8bit-Wertebereich braucht. Daher dachte ich nun, daß bei einer Lookup-Tabelle ebenfalls 32bit sinnvoll sind, auch wenn als Datentyp der Array-Elemente ein (u)int8_t ausreichen würde. Pustekuchen. Benchmarks haben gezeigt (C99, ARM-GCC 5.4.1 mit -O2), daß die Tabellen in 8bit schneller sind, jedenfalls wenn man sie abhängig von dynamischen Variablen indexiert anspricht, mit "table[foo*(bar+1)]" oder so und nicht via "*table++". Der Grund ist, daß beim Berechnen des Offsets der Index im Falle von 8bit-Tabelleninhalten direkt schon in Bytes ist, während er bei 32bit erst noch geshiftet werden muß, was nicht umsonst ist. Das Nette ist, daß man mit 8bit Speicher spart und zugleich ein bißchen Performance gewinnt.
Cortex M kann Laden/Speicher mit Angabe der Speicheradresse über ein "Basis-Register" + "Index-Register", wobei letzteres mit (1/)2/4/8 skaliert werden kann, d.h. es können Arrays aus 8-/16-/32-/64-Bit Werten direkt über eine Laufvariable (i) angesprochen werden. Dauert immer gleich lang. Falls der Compiler das weis. Bytes legt man aber schon aus Platzgründen "gepackt" an. BTW, Intel Maschinen können das auch, Mainframes (gefühlt heute nicht mehr im Einsatz, aber Gefühle täuschen manchmal ;-) können das, ... AVR's leider nicht ;-(
Carl D. schrieb: > Falls der Compiler das weis. Dann scheint es, daß GCC das zumindest in 5.4.1 nicht weiß. Oder aber das Einrichten der Skalierung kostet selber erstmal Zeit. Einen eigenen Assembler-Befehl habe ich so auf Anhieb im instruction set dafür nicht gesehen - gibt's den? > Bytes legt man aber schon aus Platzgründen "gepackt" an. Jein; mitunter ist der Platzbedarf egal, weil nur Geschwindigkeit zählt. Die Lookup-Tabellen sind übrigens genau deswegen, obwohl statisch, trotzdem ins RAM gelinkt.
Nop schrieb: > Carl D. schrieb: > >> Falls der Compiler das weis. > > Dann scheint es, daß GCC das zumindest in 5.4.1 nicht weiß. > > Oder aber das Einrichten der Skalierung kostet selber erstmal Zeit. > Einen eigenen Assembler-Befehl habe ich so auf Anhieb im instruction set > dafür nicht gesehen - gibt's den? LDR und STR und das ARM Programmierhandbuch für Cortex M(3). "Base Scaled Index" ist eine Adressierungsart. (man darf auch noch eine Konstante bis 255 dazu tun). Wobei natürlich table[foo*(Bar+1)] (2-dimensionaler wahlfreier Zugriff) damit nicht machbar ist, nur das > Der Grund ist, daß beim Berechnen des Offsets der Index im Falle von > 8bit-Tabelleninhalten direkt schon in Bytes ist, während er bei 32bit > erst noch geshiftet werden muß, was nicht umsonst ist. das Skalieren von (foo*(bar+1)) auf die Größe der Arrayelemente von 2/4/8 Bytes, das kann die Maschine selbst und umsonst.
Hi Nop, interessantes Thema, um das Ganze greifbarer zu machen, fehlen jedoch ein paar Infos, z.B.: - Hardware (M0..7) - Systemsetup (Speichermodell, Waitstates, ...) - Beispielcode ggf. Disassembly - quantitative Ergebnisse, auch bei anderen Optimierungsstufen, Zeiten und und Speicherverbrauch - Vergleich mit anderen Compilern insbesondere Keil und IAR Grüße, marcus
Carl D. schrieb: > LDR und STR und das ARM Programmierhandbuch für Cortex M(3). Ah ich habs für den M4 gefunden, Kapitel 3.4.3 "LDR and STR, register offset": op{type}{cond} Rt, [Rn, Rm {, LSL #n}] "The offset is specified by the register Rm and can be shifted left by up to 3 bits using LSL". > Wobei natürlich table[foo*(Bar+1)] (2-dimensionaler wahlfreier Zugriff) > damit nicht machbar ist Ist klar - was ich damit nur ausschließen wollte, ist ein Zugriff wie "table[3]", wo man den Shift auch schon zur Compilezeit machen könnte. > das Skalieren von (foo*(bar+1)) auf die Größe der Arrayelemente von > 2/4/8 Bytes, das kann die Maschine selbst und umsonst. Skurril, wenn ich was messe, wo ich nichts messen sollte. Muß ich mal am WE gründlicher nachforschen.
Nop schrieb: > Die Lookup-Tabellen sind übrigens genau deswegen, obwohl statisch, > trotzdem ins RAM gelinkt. Das ist gar nicht unbedingt besser, mit Pech sogar langsamer (aber dafür deterministisch), denn manche STM32 haben beim Flash ja einen Cache und der ist ja auch über einen extra Bus angebunden. Nop schrieb: > Skurril, wenn ich was messe, wo ich nichts messen sollte. Muß ich mal am > WE gründlicher nachforschen. Assembly Listing ist hier wie üblich sehr hilfreich...
Nop schrieb: > also, auf Cortex-M sind 32bit-Operationen schneller als 8bit, weil die > Maskierung entfällt. Das gilt auch beim Laden aus dem Speicher. Etwa > lokale Zähler und Schleifenvariablen sind daher als 32bit-Integer > schneller als in 8bit, auch wenn man nur den 8bit-Wertebereich braucht. Ja, diese Beobachtung habe ich für STM32 auch bereits vor ein paar Jahren gemacht. Seitdem benutze ich u.a. für Schleifenvariablen, für die ein uint8_t vom Wertebereich ausreicht, immer den uint_fast8_t Typ. Ich könnte da zwar aus Bequemlichkeit auch einfach uint32_t (oder auch unsigned int) hinschreiben, aber es gibt 2 Gründe, warum ich lieber uint_fast8_t benutze: 1. Portabilität: Wenn der Code sowohl auf STM32 als auch auf AVR laufen soll, empfiehlt sich ein uint_fast8_t, um den AVR nicht durch Verwendung eines uint32_t ins Schwitzen zu bringen. Konkrete Anwendung ist hier IRMP, welche auf vielen verschiedenen µCs zum Einsatz kommt. So läuft der Code auf allen Prozessoren mit optimaler Geschwindigkeit - egal ob 8- oder 32-Bitter. 2. Codeverständnis: Mit der Verwendung von uint_fast8_t deute ich an, dass der Wertebereich der Variablen von 0 bis 255 geht, auch wenn dann konkret auf STM32 letztendlich eine 32-Bit-Variable verwendet wird.
:
Bearbeitet durch Moderator
Marcus H. schrieb: > - Hardware (M0..7) Cortex-M4, genauer gesagt STM32F405. > - Systemsetup (Speichermodell, Waitstates, ...) Kein externer Speicher, Waitstates nach Datenblatt - 5 bei 168MHz. Dcache/Icache enabled. Also die übliche Konfiguration. > - Beispielcode ggf. Disassembly Habe ich derzeit nicht, weil das nur Teil der Applikation ist und kein eigentlicher Benchmark. Allerdings konnte ich bei einem längeren Durchlauf eine Reduktion von 605s auf 600s messen, also der Effekt ist merklich. Meßschwankungen bei den Einzelläufen sind übrigens nicht vorhanden bzw. unter einer Sekunde, also der Unterschied ist kein Rauschen. Muß ich mal ein Minimalbeispiel zusammenstellen, vielleicht ein simpel runtergehacktes "Game of Life" oder so, mal mit einem Array aus uint8_t, mal mit uint32_t. > - quantitative Ergebnisse, auch bei anderen Optimierungsstufen, Zeiten Ließe sich machen. Wobei meine Erfahrung ist, daß man -O3 gar nicht erst testen braucht, weil es sowieso langsamer als -O2 ist. > und Speicherverbrauch Ist für mich nicht relevant, mich interessiert nur die Geschwindigkeit. > - Vergleich mit anderen Compilern insbesondere Keil und IAR Fällt aus, weil ich die nicht habe - bei Keil ist die Gratisversion zu begrenzt, und bei IAR weiß ich nichtmal, ob es überhaupt eine gibt. Nur für einen Test setze ich aber keine Toolchain von Compilern auf, die ich sonst ohnehin nicht einsetzen kann. Andererseits, mit dem (noch zu erstellenden) Minimalbeispiel könnten das ja auch andere machen, die die Toolchains ohnehin schon benutzen.
Dr. Sommer schrieb: > denn manche STM32 haben beim Flash ja einen Cache Der Dache, ja, aber der wirkt nicht bei scattered access, und selbst wenn, wäre das jedesmal ein Cache Miss, weil bis zum nächsten Lookup noch einiges passiert.
@Nop Danke für Dein Feedback. Es tut gut, wenn die Bemühung um objektive Berichterstattung honoriert wird. Ich hatte vor Jahren mal einen Artikel für eine Zeitschrift geschrieben. Es ging um CoreMark 1.0 auf STM32F1 / STM32F4. Wir sind zwar mittlerweile zig Compilerversionen weiter, aber vielleicht vermittelt der Text immer noch ein bisserl Gefühl für die verschiedenen Konfigurationen. Ich verwende aktuell meist Atollic/GCC. Dadurch kann ich dem Kunden für Wartungszwecke ein komplettes, kostenloses Entwicklungssystem in die Hand geben. Allerdings würde ich, wenn's bei großen Stückzahlen auf der MCU eng wird, Testläufe mit IAR bzw. Keil machen. Und ja, die bieten Teststellungen. http://www.harerod.de/links_ger.html -> CoreMark_STM32 - Testbericht über CoreMark 1.0 Läufe auf STM32 mit verschiedenen Compilereinstellungen.
So, hier mal Ergebnisse. Interessant ist, daß signed und unsigned unterschiedlich schnell gehen, sobald man mit O2 optimiert. Da ich ursprünglich nur signed hatte, ist mir auch nur die Beschleunigung beim Übergang auf 8 bit aufgefallen; bei unsigned wäre es aber umgedreht gewesen. Wer das noch mit anderen Compilern oder so probieren will: Quelltext liegt bei, kann man in jedes Framework recht einfach reinsetzen. Eine Demo-main() für PC liegt auch bei. Die erzeugten Assembler-Listings für alle 4 Varianten (signed/unsigned, O2/O0) ebenfalls. GCC: arm-none-eabi 5.4.1 Optionen: -Wall -std=c99 -mcpu=cortex-m4 -mtune=cortex-m4 -g -mthumb STM32F405, dCache/iCache enabled, ART enabled, 168 MHz, 5 flash waitstates NUM_GENS 100000ul -O02 unsigned: 8 bit: 28.04 s 32 bit: 27.43 s signed: 8 bit: 26.82 s 32 bit: 28.04 s -O00 unsigned: 8 bit: 97.50 s 32 bit: 100.55 s signed: 8 bit: 97.50 s 32 bit: 100.55 s Und für die, die nur eben den Code angucken wollen, ohne das runterzuladen:
1 | /************************************************************************* |
2 | "Conway's Game of Life" benchmark for 8 bit and 32 bit data types. |
3 | The calculations start with a pseudo-random, but reproducible world. The |
4 | calculations are identical in every benchmark call with the same number |
5 | of generations. |
6 | |
7 | resource requirements: |
8 | needs about 8 kB RAM for the "game of life" worlds at 32x32 cells. |
9 | |
10 | external requirements: |
11 | needs the following get-time function: |
12 | int32_t Get_Time(void); |
13 | |
14 | benchmark functions: |
15 | int32_t Gol_Benchmark_8(uint32_t num_generations); |
16 | int32_t Gol_Benchmark_32(uint32_t num_generations); |
17 | |
18 | SIGNED_TEST: if this define is enabled, signed integers are used. |
19 | |
20 | input: desired number of generations. |
21 | |
22 | output: difference between Get_Time() at start and end, in whatever |
23 | unit the Get_Time() function is using. The initialization |
24 | is not part of the benchmark. |
25 | |
26 | side effects: the volatile global variable "prevent_compiler_opt" is set |
27 | to the final state of the middle "game of life" cell. This |
28 | keeps the compiler from optimizing the benchmark away. |
29 | |
30 | limitations: cell 0 is not updated, and the border wrapping is strange, |
31 | but it doesn't matter for benchmarking. |
32 | *************************************************************************/ |
33 | |
34 | #include <stdint.h> |
35 | |
36 | #define SIGNED_TEST |
37 | |
38 | #ifdef SIGNED_TEST |
39 | #define INT8 int8_t |
40 | #define INT32 int32_t |
41 | #else |
42 | #define INT8 uint8_t |
43 | #define INT32 uint32_t |
44 | #endif |
45 | |
46 | #define DEAD 0 |
47 | #define ALIVE 1U |
48 | |
49 | /*must be powers of 2*/ |
50 | #define ROWS 32UL |
51 | #define COLS 32UL |
52 | |
53 | #define WRAP_MASK (ROWS * COLS - 1UL) |
54 | |
55 | extern int32_t Get_Time(void); |
56 | |
57 | volatile INT32 prevent_compiler_opt; |
58 | |
59 | /*reuse the worlds for the 8 bit test via pointer casting*/ |
60 | static INT32 world_0[ROWS * COLS]; |
61 | static INT32 world_1[ROWS * COLS]; |
62 | |
63 | static uint32_t rand_state; |
64 | |
65 | static uint32_t Random(uint32_t max_val) { |
66 | rand_state *= 1103515245UL; |
67 | rand_state += 12345UL; |
68 | return((rand_state >> 16U) % max_val); |
69 | } |
70 | |
71 | static void Gol_Init_8(INT8 *world_8) { |
72 | uint32_t cnt; |
73 | rand_state = 42UL; /*pseudo-random, reproducible init*/ |
74 | for (cnt = 0; cnt < ROWS * COLS; cnt++) |
75 | world_8[cnt] = (INT8) Random(ALIVE+1UL); |
76 | } |
77 | |
78 | static void Gol_Init_32(INT32 *world_32) { |
79 | uint32_t cnt; |
80 | rand_state = 42UL; /*pseudo-random, reproducible init*/ |
81 | for (cnt = 0; cnt < ROWS * COLS; cnt++) |
82 | world_32[cnt] = (INT32) Random(ALIVE+1UL); |
83 | } |
84 | |
85 | static void Gol_Iteration_8(const INT8 *old_world, INT8 *new_world) { |
86 | uint32_t base_cell; |
87 | /*some simplifications here to avoid benchmarking the loop overhead |
88 | or the wrapping. |
89 | cell 0 is not updated, and the wrapping is strange, but it doesn't |
90 | matter for benchmarking.*/ |
91 | for (base_cell = ROWS * COLS - 1UL; base_cell != 0; base_cell--) { |
92 | INT32 cell_state, living_adjacents; |
93 | |
94 | /*counting from upper left adjacent, clockwise*/ |
95 | living_adjacents = old_world[(base_cell - COLS - 1UL) & WRAP_MASK]; |
96 | living_adjacents += old_world[(base_cell - COLS ) & WRAP_MASK]; |
97 | living_adjacents += old_world[(base_cell - COLS + 1UL) & WRAP_MASK]; |
98 | living_adjacents += old_world[(base_cell + 1UL) & WRAP_MASK]; |
99 | living_adjacents += old_world[(base_cell + COLS + 1UL) & WRAP_MASK]; |
100 | living_adjacents += old_world[(base_cell + COLS ) & WRAP_MASK]; |
101 | living_adjacents += old_world[(base_cell + COLS - 1UL) & WRAP_MASK]; |
102 | living_adjacents += old_world[(base_cell - 1UL) & WRAP_MASK]; |
103 | |
104 | cell_state = old_world[base_cell]; |
105 | if (cell_state == DEAD) { |
106 | if (living_adjacents == 3) /*cell born*/ |
107 | new_world[base_cell] = ALIVE; |
108 | else |
109 | new_world[base_cell] = DEAD; |
110 | } else { /*cell has been alive*/ |
111 | if ((living_adjacents == 2) || (living_adjacents == 3)) |
112 | new_world[base_cell] = ALIVE; /*cell survives*/ |
113 | else |
114 | new_world[base_cell] = DEAD; /*cell dies*/ |
115 | } |
116 | } |
117 | } |
118 | |
119 | static void Gol_Iteration_32(const INT32 *old_world, INT32 *new_world) { |
120 | uint32_t base_cell; |
121 | /*some simplifications here to avoid benchmarking the loop overhead |
122 | or the wrapping. |
123 | cell 0 is not updated, and the wrapping is strange, but it doesn't |
124 | matter for benchmarking.*/ |
125 | for (base_cell = ROWS * COLS - 1UL; base_cell != 0; base_cell--) { |
126 | INT32 living_adjacents, cell_state; |
127 | |
128 | /*counting from upper left adjacent, clockwise*/ |
129 | living_adjacents = old_world[(base_cell - COLS - 1UL) & WRAP_MASK]; |
130 | living_adjacents += old_world[(base_cell - COLS ) & WRAP_MASK]; |
131 | living_adjacents += old_world[(base_cell - COLS + 1UL) & WRAP_MASK]; |
132 | living_adjacents += old_world[(base_cell + 1UL) & WRAP_MASK]; |
133 | living_adjacents += old_world[(base_cell + COLS + 1UL) & WRAP_MASK]; |
134 | living_adjacents += old_world[(base_cell + COLS ) & WRAP_MASK]; |
135 | living_adjacents += old_world[(base_cell + COLS - 1UL) & WRAP_MASK]; |
136 | living_adjacents += old_world[(base_cell - 1UL) & WRAP_MASK]; |
137 | |
138 | cell_state = old_world[base_cell]; |
139 | if (cell_state == DEAD) { |
140 | if (living_adjacents == 3) /*cell born*/ |
141 | new_world[base_cell] = ALIVE; |
142 | else |
143 | new_world[base_cell] = DEAD; |
144 | } else { /*cell has been alive*/ |
145 | if ((living_adjacents == 2) || (living_adjacents == 3)) |
146 | new_world[base_cell] = ALIVE; /*cell survives*/ |
147 | else |
148 | new_world[base_cell] = DEAD; /*cell dies*/ |
149 | } |
150 | } |
151 | } |
152 | |
153 | /*the 8 bit benchmark API function*/ |
154 | int32_t Gol_Benchmark_8(uint32_t num_generations) { |
155 | INT8 *old_world, *new_world; |
156 | int32_t start_time, end_time; |
157 | uint32_t cnt; |
158 | /*set up buffers*/ |
159 | old_world = (INT8 *) world_0; |
160 | new_world = (INT8 *) world_1; |
161 | /*init time does not count*/ |
162 | Gol_Init_8(old_world); |
163 | start_time = Get_Time(); |
164 | |
165 | for (cnt = num_generations; cnt != 0; cnt--) { |
166 | INT8 *tmp_ptr; |
167 | Gol_Iteration_8(old_world, new_world); |
168 | /*switch buffers*/ |
169 | tmp_ptr = old_world; |
170 | old_world = new_world; |
171 | new_world = tmp_ptr; |
172 | } |
173 | |
174 | end_time = Get_Time(); |
175 | prevent_compiler_opt = old_world[ROWS * COLS / 2UL]; |
176 | return(end_time - start_time); |
177 | } |
178 | |
179 | /*the 32 bit benchmark API function*/ |
180 | int32_t Gol_Benchmark_32(uint32_t num_generations) { |
181 | INT32 *old_world, *new_world; |
182 | int32_t start_time, end_time; |
183 | uint32_t cnt; |
184 | /*set up buffers*/ |
185 | old_world = world_0; |
186 | new_world = world_1; |
187 | /*init time does not count*/ |
188 | Gol_Init_32(old_world); |
189 | start_time = Get_Time(); |
190 | |
191 | for (cnt = num_generations; cnt != 0; cnt--) { |
192 | INT32 *tmp_ptr; |
193 | Gol_Iteration_32(old_world, new_world); |
194 | /*switch buffers*/ |
195 | tmp_ptr = old_world; |
196 | old_world = new_world; |
197 | new_world = tmp_ptr; |
198 | } |
199 | |
200 | end_time = Get_Time(); |
201 | prevent_compiler_opt = old_world[ROWS * COLS / 2UL]; |
202 | return(end_time - start_time); |
203 | } |
Nico W. schrieb: > Könntest du das ganze noch mit -Os machen? Os und O03 funktionieren leider nicht, weil GCC dann anfängt herumzuspinnen und irgendwelche Library-Funktionen einfügen möchte, die ich gar nicht aufrufe. Das führt dann zu unresolved symbols und Linkerfehlern.
Gut, dann kau ich das einmal neu durch. Das liegt sicher nicht am GCC. Ich habe hier nen STM32F411 auf 100MHz. GCC 6.2.1
1 | signed: |
2 | |
3 | | O0 | Os | O1 | O2 | O3 |
4 | ------|--------|--------|--------|--------|------- |
5 | 8bit | 88.10s | 28.82s | 29.64s | 23.73s | 22.67s |
6 | 32bit | 90.12s | 33.97s | 32.79s | 25.69s | 26.78s |
7 | |
8 | unsigned: |
9 | |
10 | | O0 | Os | O1 | O2 | O3 |
11 | ------|--------|--------|--------|--------|------- |
12 | 8bit | 88.12s | 30.88s | 32.84s | 21.73s | 21.71s |
13 | 32bit | 87.06s | 29.86s | 33.79s | 28.76s | 23.72s |
:
Bearbeitet durch User
Nico W. schrieb: > Gut, dann kau ich das einmal neu durch. Das liegt sicher nicht am GCC. Vermutlich doch, weil ich ohne C-Standard-Bibliothek linke, GCC aber bei Os und O3 selber einfach unaufgefordert Funktionen davon reinnimmt. Das fällt natürlich auf die Nase. Linkst Du die Standardbibliothek mit rein? Deine Ergebnisse sind relativ ähnlich, nur bei O0/unsigned hat 6.2.1 wohl aufgeholt, dafür aber bei O2/unsigned ganz stark nachgelassen. Das sieht nach einer Regression aus, denn da ist 8bit ja sagenhafte 32% schneller. Da, wie oben im Thread dargelegt, die Adressierung eigentlich gratis sein sollte, ist das schon bemerkenswert.
Nop schrieb: > Linkst Du die Standardbibliothek mit rein? Wieder was gelernt. Ich hab hier zumindest kein -nostdinc im Makefile, falls es das ist was du meinst. Ansonsten darfst du nicht vergessen, das mein Prozessor nur mit 100MHz läuft. Deiner aber mit 168MHz. Als Basis hab ich jetzt einfach meinen Port der Teacup-Firmware genommen. Falls du genau sehen willst, wie das Makefile aussieht: https://github.com/Traumflug/Teacup_Firmware/blob/arm-stm32f411-port/Makefile-ARM
Nico W. schrieb: > Wieder was gelernt. Ich hab hier zumindest kein -nostdinc im Makefile, > falls es das ist was du meinst. Ich habe "-nostartfiles -nodefaultlibs -ffreestanding" mit im Linkeraufruf, das dürfte der verantwortliche Unterschied sein. > Ansonsten darfst du nicht vergessen, das mein Prozessor nur mit 100MHz > läuft. Deiner aber mit 168MHz. Das ist klar, aber mit den 32% meinte ich bei Dir den Unterschied innerhalb Deiner Meßergebnisse: 8bit/21.7s, 32bit/28.7s. Das ist wesentlich mehr Unterschied, als ich mit GCC 5.4.1 bei O2/unsigned zwischen 8bit/32bit messe.
Nop schrieb: > Ich habe "-nostartfiles -nodefaultlibs -ffreestanding" mit im Irks: IIRC deaktiviert -ffreestanding etliche interne Builtins und Optimierungen, z.B. wird bei strlen("Hallo") das strlen() aufgerufen und nicht durch eine Konstante ersetzt. Nimm das mal raus...
Jim M. schrieb: > Irks: IIRC deaktiviert -ffreestanding etliche interne Builtins und > Optimierungen, Laut GCC-Doku ist ffreestanding dafür gedacht: "A freestanding environment is one in which the standard library may not exist, and program startup may not necessarily be at main." Genau deswegen habe ich das drin. Das Problem ist auf Os/O3 eher, daß GCC -ffreestanding gepflegt ignoriert und trotzdem versucht, mir Funktionen aus der Runtime-Lib reinzudrücken. > z.B. wird bei strlen("Hallo") das strlen() aufgerufen und > nicht durch eine Konstante ersetzt. Bei den wenigen Gelegenheiten, wo ich die Länge eines Strings brauche, ist der sowieso von unbekannter Länge.
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
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.