Es gab viele (leicht oder ganz) falsche Vermutungen in Kommentaren zu einigen Details / Hintergründen dafür.
Sie sehen die optimierte C-Fallback-optimierte Implementierung von glibc. (Für ISAs ohne handgeschriebene asm-Implementierung) . Oder eine alte Version dieses Codes, die sich noch im glibc-Quellbaum befindet. https://code.woboq.org/userspace/glibc/string/strlen.c.html ist ein Code-Browser, der auf dem aktuellen Glibc-Git-Baum basiert. Anscheinend wird es immer noch von einigen Mainstream-Glibc-Zielen verwendet, einschließlich MIPS. (Danke @zwol).
Auf gängigen ISAs wie x86 und ARM verwendet glibc handgeschriebenen asm
Der Anreiz, etwas an diesem Code zu ändern, ist also geringer als Sie vielleicht denken.
Dieser Bithack-Code ( https://graphics.stanford.edu/~seander/bithacks.html#ZeroInWord ) ist nicht das, was tatsächlich auf Ihrem Server / Desktop / Laptop / Smartphone ausgeführt wird. Es ist besser als eine naive Byte-zu-Zeit-Schleife, aber selbst dieser Bithack ist ziemlich schlecht im Vergleich zu einem effizienten ASM für moderne CPUs (insbesondere x86, wo AVX2 SIMD das Überprüfen von 32 Bytes mit ein paar Anweisungen ermöglicht und 32 bis 64 Bytes pro Takt zulässt Zyklus in der Hauptschleife, wenn Daten im L1d-Cache auf modernen CPUs mit 2 / Takt-Vektorlast und ALU-Durchsatz heiß sind, dh für mittelgroße Zeichenfolgen, bei denen der Startaufwand nicht dominiert.)
glibc verwendet dynamische Verknüpfungstricks, um strlen
eine optimale Version für Ihre CPU zu finden. Selbst innerhalb von x86 gibt es eine SSE2-Version (16-Byte-Vektoren, Baseline für x86-64) und eine AVX2-Version (32-Byte-Vektoren).
x86 verfügt über eine effiziente Datenübertragung zwischen Vektor- und Allzweckregistern, was es einzigartig (?) macht, SIMD zu verwenden, um Funktionen für Zeichenfolgen mit impliziter Länge zu beschleunigen, bei denen die Schleifensteuerung datenabhängig ist. pcmpeqb
/ pmovmskb
ermöglicht das gleichzeitige Testen von 16 separaten Bytes.
glibc hat eine AArch64-Version wie die mit AdvSIMD und eine Version für AArch64-CPUs, bei der Vektor-> GP-Register die Pipeline blockieren, sodass dieser Bithack tatsächlich verwendet wird . Verwendet jedoch die Anzahl der führenden Nullen, um das Byte innerhalb des Registers zu finden, sobald es einen Treffer erhält, und nutzt die effizienten nicht ausgerichteten Zugriffe von AArch64, nachdem nach Seitenkreuzungen gesucht wurde.
Ebenfalls verwandt: Warum ist dieser Code bei aktivierten Optimierungen 6,5-mal langsamer? hat einige weitere Details darüber, was in x86 asm schnell und langsam ist, strlen
mit einem großen Puffer und einer einfachen asm-Implementierung, die für gcc hilfreich sein kann, um zu wissen, wie man inline ist. (Einige gcc-Versionen sind unklug inline, rep scasb
was sehr langsam ist, oder ein 4-Byte-Bithack wie dieser. Daher muss das Inline-Strlen-Rezept von GCC aktualisiert oder deaktiviert werden.)
Asm hat kein "undefiniertes Verhalten" im C-Stil . Es ist sicher, auf Bytes im Speicher zuzugreifen, wie Sie möchten, und eine ausgerichtete Last, die gültige Bytes enthält, kann keinen Fehler verursachen. Der Speicherschutz erfolgt durch Granularität der ausgerichteten Seiten. Ausgerichtete Zugriffe, die schmaler sind, können eine Seitengrenze nicht überschreiten. Ist es sicher, über das Ende eines Puffers innerhalb derselben Seite auf x86 und x64 hinaus zu lesen? Die gleiche Überlegung gilt für den Maschinencode, den dieser C-Hack von Compilern für eine eigenständige Nicht-Inline-Implementierung dieser Funktion erstellt.
Wenn ein Compiler Code zum Aufrufen einer unbekannten Nicht-Inline-Funktion ausgibt, muss er davon ausgehen, dass die Funktion alle globalen Variablen und den Speicher ändert, auf den er möglicherweise einen Zeiger hat. Das heißt, alles außer Einheimischen, deren Adresse nicht entkommen ist, muss während des Anrufs im Speicher synchronisiert sein. Dies gilt natürlich für in asm geschriebene Funktionen, aber auch für Bibliotheksfunktionen. Wenn Sie die Optimierung der Verbindungszeit nicht aktivieren, gilt dies sogar für separate Übersetzungseinheiten (Quelldateien).
Warum dies als Teil von glibc sicher ist, aber nicht anders.
Der wichtigste Faktor ist, dass dies strlen
zu nichts anderem führen kann. Dafür ist es nicht sicher. Es enthält UB mit striktem Aliasing (Lesen von char
Daten durch ein unsigned long*
). char*
darf alles andere aliasen, aber das Gegenteil ist nicht der Fall .
Dies ist eine Bibliotheksfunktion für eine vorab kompilierte Bibliothek (glibc). Bei der Optimierung der Verbindungszeit für Anrufer wird dies nicht berücksichtigt. Dies bedeutet, dass nur ein sicherer Maschinencode für eine eigenständige Version von kompiliert werden muss strlen
. Es muss nicht tragbar / sicher sein C.
Die GNU C-Bibliothek muss nur mit GCC kompiliert werden. Anscheinend wird es nicht unterstützt , es mit clang oder ICC zu kompilieren, obwohl sie GNU-Erweiterungen unterstützen. GCC ist ein früherer Compiler, der eine C-Quelldatei in eine Objektdatei mit Maschinencode umwandelt. Kein Interpreter. Wenn er also nicht zur Kompilierungszeit inline ist, sind Bytes im Speicher nur Bytes im Speicher. dh striktes Aliasing UB ist nicht gefährlich, wenn die Zugriffe mit unterschiedlichen Typen in unterschiedlichen Funktionen erfolgen, die nicht ineinander greifen.
Denken Sie daran, dass strlen
das Verhalten durch den ISO C-Standard definiert ist. Dieser Funktionsname ist speziell Teil der Implementierung. Compiler wie GCC behandeln den Namen sogar als integrierte Funktion, sofern Sie ihn nicht verwenden -fno-builtin-strlen
. Dies strlen("foo")
kann eine Konstante für die Kompilierungszeit sein 3
. Die Definition in der Bibliothek wird nur verwendet, wenn gcc beschließt, tatsächlich einen Aufruf an sie zu senden, anstatt ein eigenes Rezept oder etwas anderes einzufügen.
Wenn UB zur Kompilierungszeit für den Compiler nicht sichtbar ist , erhalten Sie einen vernünftigen Maschinencode. Der Maschinencode muss Arbeit für den nicht-UB Fall, und selbst wenn man wollte , gibt es keine Möglichkeit für die asm zu erkennen , welche Arten der Anrufer verwendet , um Daten zu setzen in den Spitz in dem Speicher.
Glibc wird zu einer eigenständigen statischen oder dynamischen Bibliothek kompiliert, die nicht mit der Optimierung der Verbindungszeit kompatibel ist. Die Build-Skripte von glibc erstellen keine "fetten" statischen Bibliotheken, die Maschinencode + gcc enthalten. GIMPLE-interne Darstellung zur Optimierung der Verbindungszeit beim Inlining in ein Programm. (dh libc.a
nicht an der -flto
Optimierung der Verbindungszeit im Hauptprogramm teilnehmen.) Das Erstellen von glibc auf diese Weise wäre für Ziele, die dies tatsächlich verwenden.c
, möglicherweise unsicher .
Wie @zwol kommentiert, kann LTO beim Erstellen von glibc selbst nicht verwendet werden , da "spröder" Code wie dieser beschädigt werden kann , wenn Inlining zwischen glibc-Quelldateien möglich ist. (Es gibt einige interne Verwendungen von strlen
, z. B. als Teil der printf
Implementierung)
Dies strlen
macht einige Annahmen:
CHAR_BIT
ist ein Vielfaches von 8 . Richtig auf allen GNU-Systemen. POSIX 2001 garantiert sogar CHAR_BIT == 8
. (Dies sieht für Systeme mit CHAR_BIT= 16
oder 32
wie einige DSPs sicher aus. Die Schleife für nicht ausgerichtete Prologe führt immer 0 Iterationen aus, wenn sizeof(long) = sizeof(char) = 1
jeder Zeiger immer ausgerichtet ist und p & sizeof(long)-1
immer Null ist.) Wenn Sie jedoch einen Nicht-ASCII-Zeichensatz mit Zeichen 9 hatten oder 12 Bit breit, 0x8080...
ist das falsche Muster.
- (vielleicht)
unsigned long
ist 4 oder 8 Bytes. Oder vielleicht würde es tatsächlich für jede Größe von unsigned long
bis zu 8 funktionieren , und es wird ein verwendet assert()
, um dies zu überprüfen.
Diese beiden sind UB nicht möglich, sie sind nur für einige C-Implementierungen nicht portierbar. Dieser Code ist (oder war) Teil der C-Implementierung auf Plattformen, auf denen er funktioniert. Das ist also in Ordnung.
Die nächste Annahme ist das Potenzial C UB:
- Eine ausgerichtete Last, die gültige Bytes enthält, kann keine Fehler verursachen und ist sicher, solange Sie die Bytes außerhalb des gewünschten Objekts ignorieren. (Richtig in asm auf allen GNU-Systemen und auf allen normalen CPUs, da der Speicherschutz mit Granularität der ausgerichteten Seiten erfolgt. Ist es sicher, über das Ende eines Puffers innerhalb derselben Seite auf x86 und x64 zu lesen? Sicher in C, wenn die UB ist zur Kompilierungszeit nicht sichtbar. Ohne Inlining ist dies hier der Fall. Der Compiler kann nicht beweisen, dass das Lesen nach dem ersten
0
UB ist; es könnte sich um ein C- char[]
Array handeln, das {1,2,0,3}
beispielsweise enthält.)
Dieser letzte Punkt macht es sicher, hier über das Ende eines C-Objekts hinaus zu lesen. Das ist ziemlich sicher, selbst wenn es mit aktuellen Compilern inline ist, da ich denke, dass sie derzeit nicht behandeln, dass ein Ausführungspfad nicht erreichbar ist. Trotzdem ist das strikte Aliasing bereits ein Showstopper, wenn Sie dies jemals inline lassen.
Dann hätten Sie Probleme wie das alte unsichere memcpy
CPP-Makro des Linux-Kernels , für das Zeiger-Casting verwendet wurde unsigned long
( gcc, striktes Aliasing und Horrorgeschichten ).
Dies strlen
geht auf die Zeit zurück, in der man mit solchen Dingen im Allgemeinen davonkommen konnte . Früher war es ziemlich sicher ohne die Einschränkung "nur wenn nicht inliniert" vor GCC3.
UB, das nur sichtbar ist, wenn wir über Anruf- / Ret-Grenzen schauen, kann uns nicht schaden. (zB das Aufrufen von a char buf[]
anstelle eines Arrays von unsigned long[]
Cast zu a const char*
). Sobald der Maschinencode in Stein gemeißelt ist, handelt es sich nur noch um Bytes im Speicher. Bei einem Nicht-Inline-Funktionsaufruf muss davon ausgegangen werden, dass der Angerufene den gesamten Speicher liest.
Schreiben Sie dies sicher, ohne UB strikt zu aliasen
Das GCC-Typattributmay_alias
gibt einem Typ den gleichen Alias - alles wie char*
. (Vorgeschlagen von @KonradBorowsk). GCC-Header verwenden es derzeit für x86-SIMD-Vektortypen, __m128i
sodass Sie dies immer sicher tun können _mm_loadu_si128( (__m128i*)foo )
. ( Weitere Informationen dazu, was dies bedeutet und was nicht, finden Sie unter Ist "Neuinterpretation_casting" zwischen dem Hardwarevektorzeiger und dem entsprechenden Typ ein undefiniertes Verhalten? )
strlen(const char *char_ptr)
{
typedef unsigned long __attribute__((may_alias)) aliasing_ulong;
aliasing_ulong *longword_ptr = (aliasing_ulong *)char_ptr;
for (;;) {
unsigned long ulong = *longword_ptr++; // can safely alias anything
...
}
}
Sie können auch aligned(1)
einen Typ mit ausdrücken alignof(T) = 1
.
typedef unsigned long __attribute__((may_alias, aligned(1))) unaligned_aliasing_ulong;
Eine tragbare Möglichkeit, eine Aliasing-Last in ISO auszudrücken, besteht darinmemcpy
, dass moderne Compiler wissen, wie sie als einzelne Ladeanweisung inline sind. z.B
unsigned long longword;
memcpy(&longword, char_ptr, sizeof(longword));
char_ptr += sizeof(longword);
Dies funktioniert auch für nicht ausgerichtete Lasten, da dies memcpy
wie bei char
einem Zugriff von Zeit zu Zeit funktioniert . In der Praxis verstehen moderne Compiler dies jedoch memcpy
sehr gut.
Hier besteht die Gefahr , dass , wenn GCC nicht wissen sicher , dass char_ptr
wortausgerichtet ist, es wird nicht auf einigen Plattformen Inline , die nicht unaligned Lasten in asm unterstützen könnten. zB MIPS vor MIPS64r6 oder älterem ARM. Wenn Sie einen tatsächlichen Funktionsaufruf erhalten, um memcpy
nur ein Wort zu laden (und es in einem anderen Speicher zu belassen), wäre dies eine Katastrophe. GCC kann manchmal sehen, wenn Code einen Zeiger ausrichtet. Oder nach der Char-at-a-Time-Schleife, die eine lange Grenze erreicht, die Sie verwenden können
p = __builtin_assume_aligned(p, sizeof(unsigned long));
Dies vermeidet nicht das mögliche UB zum Vorlesen des Objekts, aber mit dem aktuellen GCC ist dies in der Praxis nicht gefährlich.
Warum eine handoptimierte C-Quelle erforderlich ist: Aktuelle Compiler sind nicht gut genug
Handoptimierter ASM kann sogar noch besser sein, wenn Sie den letzten Leistungsabfall für eine weit verbreitete Standardbibliotheksfunktion wünschen. Besonders für so etwas memcpy
, aber auch strlen
. In diesem Fall wäre es nicht viel einfacher, C mit x86-Intrinsics zu verwenden, um SSE2 zu nutzen.
Aber hier geht es nur um eine naive vs. bithack C-Version ohne ISA-spezifische Funktionen.
(Ich denke, wir können davon ausgehen strlen
, dass es wichtig genug ist, es so schnell wie möglich laufen zu lassen. Daher stellt sich die Frage, ob wir effizienten Maschinencode aus einer einfacheren Quelle erhalten können. Nein, das können wir nicht.)
Aktuelle GCC und Clang sind nicht in der Lage, Schleifen automatisch zu vektorisieren, bei denen die Anzahl der Iterationen vor der ersten Iteration nicht bekannt ist . (z . B. muss geprüft werden können, ob die Schleife mindestens 16 Iterationen ausführen soll, bevor die erste Iteration ausgeführt wird.) z. B. ist die automatische Verankerung von memcpy möglich (Puffer mit expliziter Länge), jedoch nicht strcpy oder strlen (Zeichenfolge mit impliziter Länge), wenn der aktuelle Wert angegeben ist Compiler.
Dies schließt Suchschleifen oder jede andere Schleife mit einem datenabhängigen if()break
sowie einem Zähler ein.
ICC (Intels Compiler für x86) kann einige Suchschleifen automatisch vektorisieren, erstellt jedoch immer noch nur naive Bytes für einen einfachen / naiven C, strlen
wie ihn OpenBSDs libc verwendet. ( Godbolt ). (Aus der Antwort von @ Peske ).
strlen
Für die Leistung mit aktuellen Compilern ist eine handoptimierte libc erforderlich . Es ist erbärmlich, jeweils 1 Byte auf einmal zu arbeiten (wobei möglicherweise 2 Bytes pro Zyklus auf breiten superskalaren CPUs abgewickelt werden), wenn der Hauptspeicher mit etwa 8 Bytes pro Zyklus Schritt halten kann und der L1d-Cache 16 bis 64 Bytes pro Zyklus liefern kann. (2x 32-Byte-Ladevorgänge pro Zyklus auf modernen Mainstream-x86-CPUs seit Haswell und Ryzen. AVX512 wird nicht berücksichtigt, wodurch die Taktraten nur für die Verwendung von 512-Bit-Vektoren reduziert werden können. Deshalb hat glibc es wahrscheinlich nicht eilig, eine AVX512-Version hinzuzufügen Obwohl mit 256-Bit-Vektoren, wird AVX512VL + BW maskiert in eine Maske verglichen und / ktest
oder kortest
könnte das strlen
Hyperthreading freundlicher machen, indem die Uops / Iteration reduziert wird.)
Ich schließe hier Nicht-x86 ein, das sind die "16 Bytes". Zum Beispiel können die meisten AArch64-CPUs zumindest das, denke ich, und einige sicherlich mehr. Und einige haben genug Ausführungsdurchsatz strlen
, um mit dieser Lastbandbreite Schritt zu halten.
Natürlich sollten Programme, die mit großen Zeichenfolgen arbeiten, normalerweise die Längen verfolgen, um zu vermeiden, dass die Länge von C-Zeichenfolgen mit impliziter Länge sehr häufig ermittelt werden muss. Die Leistung von kurzer bis mittlerer Länge profitiert jedoch immer noch von handgeschriebenen Implementierungen, und ich bin sicher, dass einige Programme Strlen für Zeichenfolgen mittlerer Länge verwenden.