Hallo zusammen,
vorweg mir geht es darum die Funktion von strlen zu verstehen. Bzw. wie
genau der Ablauf dieser Funktion ist. Ich schaue da nicht ganz hinter,
würde mich freuen, wenn ihr mir auf die Sprünge helfen könntent :)
Also geht um dieses Snippet:
1
size_t__cdeclstrlen(constchar*text){
2
constchar*s;
3
constuint32_t*pdwText;
4
registeruint32_tdwText;
5
6
pdwText=(uint32_t*)text;
7
while(1){
8
dwText=*pdwText;
9
10
if((dwText-0x01010101UL)&~dwText&0x80808080UL){
11
s=(constchar*)pdwText;
12
while(*s)s++;
13
returns-text;
14
}
15
16
pdwText++;
17
}
18
}
Es stammt von dieser Seite:
https://fredmameri.wordpress.com/2011/04/24/on-the-performance-of-strlen/
Ich versuche es mal mit meinen Worten zu erklären.
Sagen wir mal ich definiere einen String(Char Array) der den Inhalt
"12345678"+(\0) hat, das wären ja 8 Bytes.
Ich gehe mal die Funktion durch:
----------------------------------------------------------------------
1
size_t__cdeclstrlen(constchar*text)
- Das cdecl irritiert mich schon direkt :( Das macht der Compiler doch
schon von sich aus vor eine Funktion oder nicht?
----------------------------------------------------------------------
1
constchar*s;
2
constuint32_t*pdwText;
3
registeruint32_tdwText;
4
5
pdwText=(uint32_t*)text;
- Hier sind die Variablen Deklarationen
- der Pointer s wird benötigt für die innere Schleife
-> Dieser definiert sich aus pdwText welches dann explizit durch (const
char*) gecastet wird.
-> Kann man den nicht weglassen?! pdwText ist doch auch ein Pointer vom
Typ unsigned int, char ist doch im Prinzip auch ein Uint oder nicht?
- dwText soll mit dem Kennwort register einem Prozessor Register
zugewiesen werden z.b.(eax). Im Prinzip ist das register doch "nur eine
Empfehlung für den Compiler" oder etwa nicht?
- Der Pointer pdwText bekommt den Pointer von Text, jedoch mit einer
Breite von 4 Bytes korrekt?
-> Ich hatte ja gesagt Text sei = "12345678", das sind ja 8 Bytes.
-> Wie sieht das bitte intern aus?? pdwText[0] = 1234, pdwText[1] =
5678?
-> Erste 4 Bytes = pdwText[0], zweite 4 Bytes = pdwText[1]
----------------------------------------------------------------------
1
while(1){
2
dwText=*pdwText;
- while(1) wird niemals beendet, weil 1 eins ist, und nie Null werden
kann
- dwText was im Prozessor Register optional liegt, bekommt die "ersten"
4 Bytes vom pdwText also = "1234" bzw. die Adresse wo die ersten 4 Bytes
sind, korrekt???
----------------------------------------------------------------------
1
if((dwText-0x01010101UL)&~dwText&0x80808080UL){
- Offentsichtlich wird hier geprüft ob die 4 Bytes von dwText eine Null
('\0') enthalten.
- Absolute Verwirrung pur ... :(
-> der Adressbereich der ersten 4 Bytes wird der ersten Hex Zahl
abgezogen
-> Das Ergebnis wird bitweise verundet mit der Negation des
Adressbereich der ersten 4 Bytes
-> Und dann wird das ganze nochmal verundet mit der zweiten Hex Zahl
-> Wenn das Ergebnis 1 ist, gehts in die innere Schleife.
-> Im Testfall der ersten 4 Bytes würde er jetzt nicht hierrein springen
-> Große Verwirrung... :(
----------------------------------------------------------------------
1
s=(constchar*)pdwText;
2
while(*s)s++;
3
returns-text;
- Sagen wir mal ich wäre beim zweiten Durchlauf, also die nächsten 4
Bytes=(5678)
- s bekommt pdwText, jedoch mit einer Breite von einem Byte.
-> Dass heißt für den zweiten Durchlauf:
-> s[0] = 5, s[1] = 6, s[2] = 7, s[3] = 8, s[4] = '\0'
- Jetzt wird der Pointer solange hochgezählt bis while('\0') ist, dann
wird while beendet
- Bedeutet dann im return : Adresse von '\0' - Adresse von "12345678"
- Das return wird dann implizit als size_t gecastet, und gibt dann 8
zurück
- Bin total durcheinander :/
----------------------------------------------------------------------
1
pdwText++;
2
}
3
}
- Im ersten Durchlauf springt der hier direkt hin, und zählt den
Adressbereich des Pointers um 4 Bytes hoch(weil ja u int 32 = 4 bytes),
korrekt?
- Danach fängt er vorne an.
- Im zweiten Durchlauf kommt der gar nicht hierrein
Ich denke mal ich hab das komplett falsch verstanden, deswegen frag ich
euch ja =)
Hoffe ihr könnt mir auf die Sprünge helfen.
Herzlichen Dank
Lieben gruß
Deine Annahmen sind schon mal falsch.
1. es sind 9 Byte
2. char ist nicht zwingend uint. Das ist System und Compiler abhängig.
Den rest habe ich keine Lust durchzuspielen.
R.
also auf die schnelle kapier ich den Code auch nicht komplett, aber ich
versuch mal ein paar Tipps zu geben. Übrigens finde ich den Code SEHR
schlecht, dazu aber später mehr.
Müller schrieb:> - while(1) wird niemals beendet, weil 1 eins ist, und nie Null werden> kann
doch, weil es innerhalb des while(1) Schleife ein "return ..." gibt ;-)
Müller schrieb:> -> Kann man den nicht weglassen?! pdwText ist doch auch ein Pointer vom> Typ unsigned int, char ist doch im Prinzip auch ein Uint oder nicht?
Ein char ist erstmal 1 byte breit. Ein Int, abhängig von der Maschine
kanns mal 2 oder auch 4 Byte usw. sein. Ein Pointer drauf ist natürlich
immer gleich groß. ABER er macht den Cast explizit, da dies "guter Stil"
ist und von manchen Coding Guidelines gefordert wird.
Du kannst den Code auch mal manuell durchsteppen, dann siehst du recht
schnell was los ist.
Müller schrieb:> Offentsichtlich wird hier geprüft ob die 4 Bytes von dwText eine Null> ('\0') enthalten.> - Absolute Verwirrung pur ... :(> -> der Adressbereich der ersten 4 Bytes wird der ersten Hex Zahl> abgezogen> -> Das Ergebnis wird bitweise verundet mit der Negation des> Adressbereich der ersten 4 Bytes> -> Und dann wird das ganze nochmal verundet mit der zweiten Hex Zahl> -> Wenn das Ergebnis 1 ist, gehts in die innere Schleife.> -> Im Testfall der ersten 4 Bytes würde er jetzt nicht hierrein springen> -> Große Verwirrung... :(
Pass auf, das sind KEINE Pointer mehr! In dwText steht bereits ein Teil
deines Strings!
Der Code überprüft, ob mindestens 1 Byte davon 0 ist. Falls ja, dann
wird die innere Schleife ausgeführt. Da wird nicht mit "Adressbereichen"
gerechnet, sondern direkt mit dem Inhalt an der Adresse! Siehe Zeile
darüber: "dwText = *pdwText;"
Generell halte ich den Code aber für NICHT empfehlenswert, da er auf
anderen Architekturen Probleme machen kann:
- Er lädt grundsätzlich 4 Byte, egal wie lang der String ist. Wenn er
doch nur 3 Byte lang ist, dann liest er über das Ende hinaus. Das geht
meistens gut, aber nicht immer bzw. lässt sich das nicht wirklich
beweisen.
- Noch schwerwiegender: Beim Cast von char* auf uint32_t* ist es NICHT
mehr sichergestellt, dass das Alignment passt! Char* kann auf beliebige
Adressen zeigen, das klappt normalerweise immer. Bei uint32_t* erfordern
aber gewisse Architekturen, dass die Adresse ohne Rest durch 4 teilbar
ist. Wenn du den Code also zum Beispiel auf SPARC laufen lässt, dann
wird er dir in 3 von 4 Fällen um die Ohren fliegen und du bekommst vom
Prozessor eine unaligned access exception.
Der Code ist also nur mit Einschränkungen zu empfehlen, sei dir klar,
dass dies nicht das gelbe vom Ei ist ;-)
@Müller
So geht das nicht. Du hast so viele Lücken in den Grundlagen, dass es
angebracht ist, erst einmal diese zu füllen. Daher rührt auch Deine
überwältigende Verwirrung. Fange bei den einfachen Sachen an - nicht bei
den komplizierten.
Müller schrieb:> Hey,> Mit geht es nur um das Verständnis. Der Vorteil soll wohl sein, dass 4> Bytes gleichzeitig auf 0 geprüft werden können, denke ich.> Gruß
Da denkst Du richtig. Bedenke aber auch das der Code sowohl
Betriebssystem als auch Architektur unabhängig ist.
Und bedenke das ASCII ursprünglich nur 127 Byte hatte. Also nicht
signed.
R.
Kann jemand überblicken ob diese Optimierung Überhaupt sinnvoll ist?
schon diese 4 Rechenschritte,
1
if((dwText-0x01010101UL)&~dwText&0x80808080UL){
brauchen doch schon die 4 fache Zeit von deinen einfachen Vergleich auf
\0.
Dann sehe ich auch noch das Problem bei Stringende, wenn der String nur
char[2] ist, dann greifst er auf 2byte zu, die schon nicht mehr zum
Speicher der Variable gehören.
Peter II schrieb:> Kann jemand überblicken ob diese Optimierung Überhaupt sinnvoll ist?>> schon diese 4 Rechenschritte,>>
1
>if((dwText-0x01010101UL)&~dwText&0x80808080UL){
2
>
>> brauchen doch schon die 4 fache Zeit von deinen einfachen Vergleich auf> \0.>> Dann sehe ich auch noch das Problem bei Stringende, wenn der String nur> char[2] ist, dann greifst er auf 2byte zu, die schon nicht mehr zum> Speicher der Variable gehören.
Deshalb halte ich den gezeigten Code auch nur bei 32 bit Architekturen
für sinnvoll. Man müsste mal raussuchen, wie strlen für 8-Bit-AVR
implementiert ist...
Peter II schrieb:> Kann jemand überblicken ob diese Optimierung Überhaupt sinnvoll ist?>> schon diese 4 Rechenschritte,> if ((dwText - 0x01010101UL) & ~dwText & 0x80808080UL) {>> brauchen doch schon die 4 fache Zeit von deinen einfachen Vergleich auf> \0.
Ja, aber beim Vergleich von mehr Bytes, muss er das seltener machen.
Noch schlimmer: Wenn man Pech hat bedeutet der Zugriff auf ein char,
dass es zunächst maskiert und eventuell geshiftet werden muss.
Ich hatte es mal bei einer Kopierroutine getestet. Da brachte das einen
ziemlichen Vorteil (auf einer x86 Architektur).
apr schrieb:> Peter II schrieb:>> Kann jemand überblicken ob diese Optimierung Überhaupt sinnvoll ist?>>>> schon diese 4 Rechenschritte,>> if ((dwText - 0x01010101UL) & ~dwText & 0x80808080UL) {>>>> brauchen doch schon die 4 fache Zeit von deinen einfachen Vergleich auf>> \0.> Ja, aber beim Vergleich von mehr Bytes, muss er das seltener machen.> Noch schlimmer: Wenn man Pech hat bedeutet der Zugriff auf ein char,> dass es zunächst maskiert und eventuell geshiftet werden muss.
wo kann man denn nicht direkt auf ein Byte zugreifen?
> Ich hatte es mal bei einer Kopierroutine getestet. Da brachte das einen> ziemlichen Vorteil (auf einer x86 Architektur).
beim kopieren muss ja diese "komplizierte" Rechnung nicht gemacht
werden, dann ist ja klar das 4bytes auf einmal kopieren schneller ist
als jedes Byte einzeln.
Determine if a word has a zero byte
[...]
There is yet a faster method — use hasless(v, 1), which is defined
below; it works in 4 operations and requires no subsquent verification.
It simplifies to
#define haszero(v) (((v) - 0x01010101UL) & ~(v) & 0x80808080UL)
The subexpression (v - 0x01010101UL), evaluates to a high bit set in any
byte whenever the corresponding byte in v is zero or greater than 0x80.
The sub-expression ~v & 0x80808080UL evaluates to high bits set in bytes
where the byte of v doesn't have its high bit set (so the byte was less
than 0x80). Finally, by ANDing these two sub-expressions the result is
the high bits set where the bytes in v were zero, since the high bits
set due to a value greater than 0x80 in the first sub-expression are
masked off by the second.
Paul Messmer suggested the fast pretest improvement on October 2, 2004.
Juha Järvi later suggested hasless(v, 1) on April 6, 2005, which he
found on Paul Hsieh's Assembly Lab; previously it was written in a
newsgroup post on April 27, 1987 by Alan Mycroft.
http://graphics.stanford.edu/~seander/bithacks.html#ZeroInWord
> mir geht es darum die Funktion von strlen zu verstehen
Geht es dir darum, was strlen per Standard macht oder darum, wie das
deine Beispielimplementation erreicht?
Falls letzteres: falscher Zeitpunkt. Deine Fragen zeigen, dass du
Anfänger bist und die Implementation von strlen, die du dir rausgesucht
hast, ist eine stark optimierte Version, die, wie man hier sieht, selbst
erfahrene Programmierer aufs Glatteis führen kann (sie ist fehlerhaft).
In der Mathematik fängst du ja auch mit den Grundrechenarten an, nicht
mit der Analysis.
Wenn es dir darum geht, die Funktion von strlen anhand einer
Implementation zu "erforschen", hier zwei normale Implementationen:
Müller schrieb:> pdwText = (uint32_t*)text;
Auf einer Sparc würde das unter umständen mit Core dump enden, wegen
paging fault.
Somit ist dieser Code auch nicht portabel.
Peter II schrieb:>> Ja, aber beim Vergleich von mehr Bytes, muss er das seltener machen.>> Noch schlimmer: Wenn man Pech hat bedeutet der Zugriff auf ein char,>> dass es zunächst maskiert und eventuell geshiftet werden muss.> wo kann man denn nicht direkt auf ein Byte zugreifen?>
Keine Ahnung – Möglich wärs aber ;) (tatsächlich dachte ich ARM hätte
solche Probleme…)
>> Ich hatte es mal bei einer Kopierroutine getestet. Da brachte das einen>> ziemlichen Vorteil (auf einer x86 Architektur).> beim kopieren muss ja diese "komplizierte" Rechnung nicht gemacht> werden, dann ist ja klar das 4bytes auf einmal kopieren schneller ist> als jedes Byte einzeln.
Naja wer misst, misst Mist:
opt ist die oben geschriebene Funktion. In Klammern die größe des
Datentyps. Dann kommt die Anzahl der Elemente in "text" und die
gemessene Zeit.
Rene H. schrieb:> Auf einer Sparc würde das unter umständen mit Core dump enden, wegen> paging fault.
Die "richtigen" Implementierungen (siehe Beitrag mar IO) scheinen vorher
ein Alignment durchzuführen. Dadurch hatte man "passende" Zugriffe und
fällt auch auf keine falsche Page.
Peter II schrieb:> ...brauchen doch schon die 4 fache Zeit von deinen einfachen Vergleich
Vergleich ist nicht gleich Vergleich. Speicherzugriffe sind bspw. recht
teuer. Hatte letztens einen Fall wo 10 Operationen eine 8-Bit Lookup
Table outperformed haben (Intel x86_64).
apr schrieb:> Naja wer misst...
Interessant. Danke. :-)
Hätte nicht gedacht, dass das sooo viel bringt. Ich hätte spekuliert,
dass das Nachfüllen des Caches das Bottleneck ist und die Optimierung
daher ins Leere läuft.
Interessant ist das ganze bei uns auch gewesen in Verbindung mit Visual
C++ 2010. Da steckt in den libs auch eine strlen-Version die
schnarchlangsam ist. Hier hat die Implementierung einer eigenen Funktion
ähnlich wie ganz oben einen Faktor 10 gebracht. Es ging dabei um
Datenbankimporte von mehreren 100 Mio Datensätzen. Ob in den neueren
Versionen das anders ist haben wir aber nie untersucht. Es war auf alle
Fälle schon ein Erkenntnisgewinn festzustellen, dass strlen den
dramatischen Performanceunterschied zur Linux und OSX-Version des selben
Programmteils verursachte.
Natürlich ist nicht immer strlen in den innersten Schleifen beteiligt,
da spielt die ganze Betrachtung keine Rolle. Aber wehe sie wird
millionenfach in engen Schleifen benutzt, dann kann man schon
Überraschungen erleben.
temp schrieb:> Hier hat die Implementierung einer eigenen Funktion> ähnlich wie ganz oben einen Faktor 10 gebracht.
kann ich fast nicht glauben, wie in den Beispielen von S. J. geht es
dort maximal um Faktor 4. Zwischen der primitivsten und optimiertesten
Version.
Oder arbeitet ihr mit Wide / Unicode Strings?
Genaue Zahlen habe ich nicht mehr, dazu ist das zu lange her. Einigen
wir uns auf "gefühlte" 10. Der Unterschied war jedenfalls dramatisch.
Wir setzen jetzt unter Windows die glibc-Variante ein. z.B. hier:
https://github.com/zerovm/glibc/blob/master/string/strlen.c
S. J. schrieb:> Wollte mal gucken, ob der Cache vllt. nicht doch das Bottleneck ist. --> Ist er nicht. Wen's interessiert, anbei die Zeiten (s) für 64GiB checks.
Kann das nicht zu fehler führen?
hier wird ja mit sizeof( Long ) über den String gesprungen. Was ist wenn
der speicherbreich für die Variabel gar nicht so gross ist?
char[2] x = 'a'
dann werden doch noch 6byte gelesen die nicht mehr dazugehören. Wenn das
ganze auf einer Segmetgrenze kommt, dann kann es doch so einer
Read-Access-Violation kommen oder nicht? Oder hoft man einfach das das
nie der Fall ist?
Müller schrieb:> Es stammt von dieser Seite:
Der Code ist falsch.
Er greift bis 3 Bytes hinter das Ende des Strings zu,
das kann bei Prozessorarchitekturen mit Speicherschutz schon mal einen
Absturz ergeben,
und er greift beginnend mit dem Anfang des Strings auf 32 bit Werte zu,
das kann bei Prozessorarchitekturen deren Prozessoren nur aligned
zugreifen dürfen wie Sparc auch eine Exception geben,
denn character-Strings dürfen an jeder byte-Position beginnen.
So etwas darf man in einer Standard-Library machen die zu einem
Prozessor gehört wenn man sich sicher ist, daß er das akzeptiert, aber
nicht in selbst geschriebenem code der eines Tages mal auf eine andere
Plattform soll.
Michael B. schrieb:> Müller schrieb:>> Es stammt von dieser Seite:>> Der Code ist falsch.>> Er greift bis 3 Bytes hinter das Ende des Strings zu,> das kann bei Prozessorarchitekturen mit Speicherschutz schon mal einen> Absturz ergeben,
das machen merkwürdigerweise alle "schnellen" Implementierungen von
strlen. (was auch gar nicht anders geht, wenn man mehr als 1byte
gleichzeitig prüfen will).
Scheinbar hoffen alle, das der String nicht am ende einer Page liegt -
oder ist das irgendwo sichergestellt?
Peter II schrieb:> ... Wenn das> ganze auf einer Segmetgrenze kommt, dann kann es doch so einer> Read-Access-Violation kommen oder nicht? Oder hoft man einfach das das> nie der Fall ist?
Davor steht aber...
1
while(1){
2
if(p[0]=='\0')
3
returnp-s;
4
if(0x0==((unsignedlong)p&(sizeof(p)-1)))
5
break;
6
++p;
7
}
Wobei ich gerade sehe, dass das eine "optimierte" version ist.
(if Abfragen andersherum ist in fast allen Fällen besser)
S. J. schrieb:> Und? Was passiert dann? Welches System hat Page oder "Segmentgrenzen"> die nicht aligned sind? Nur bei denen, kann es zu Problemen kommen.
das ist ja die große Frage. C legt es zumindest nicht fest. Es könnte
auch für jede Variable eine Page in der größe der Variable vorhanden
sein.
S. J. schrieb:> Und? Was passiert dann? Welches System hat Page oder> "Segmentgrenzen"> die nicht aligned sind? Nur bei denen, kann es zu Problemen kommen.
Da der String nicht-aligned beginnen kann
x = strlen(strchr("Hallo Welt",'W'))
kann hinter der '\0' durchaus eine Page-Grenze zu Ende sein.
Peter II schrieb:> Es könnte> auch für jede Variable eine Page in der größe der Variable vorhanden> sein.
Prinzipiell hast Du Recht.
Nenn mir aber eine Architektur/OS wo Pages oder Speicherschutz nicht
aligned sind.
@ Michael Bertrandt: Guck Dir den Code an.
-> Beitrag "Re: C - Funktion von Strlen verstehen"
S. J. schrieb:> @ Michael Bertrandt: Guck Dir den Code an.> -> Beitrag "Re: C - Funktion von Strlen verstehen"
Gilt aber, wenn ich das so verstehe, nicht für die Funktion von Müller,
sondern für eine besser implementierte Libraryfunktion.
Michael B. schrieb:> S. J. schrieb:>> Und? Was passiert dann? Welches System hat Page oder>> "Segmentgrenzen">> die nicht aligned sind? Nur bei denen, kann es zu Problemen kommen.>> Da der String nicht-aligned beginnen kann>> x = strlen(strchr("Hallo Welt",'W'))>> kann hinter der '\0' durchaus eine Page-Grenze zu Ende sein.
In den korrekten Implementierungen werden die Wörter nur aligned
gelesen. Wenn hinter der '\0' die Page zu Ende ist, dann war die '\0'
das letzte Byte innerhalb des Worts. Danach wird kein weiteres Wort mehr
gelesen.
Hans schrieb:> In den korrekten Implementierungen werden die Wörter nur aligned> gelesen. Wenn hinter der '\0' die Page zu Ende ist, dann war die '\0'> das letzte Byte innerhalb des Worts. Danach wird kein weiteres Wort mehr> gelesen.
das hoffen alle, festgelegt ist das nirgends.
lach -- Die typische Page Size ist numal 4K. Kann man bestimmt auch iwo
nachlesen...
Michael B. schrieb:> Gilt aber, wenn ich das so verstehe, nicht für die Funktion von Müller,> sondern für eine besser implementierte Libraryfunktion.
Er bezog sich auf "meine" Implementierung. Und da ist das drin.