it-swarm.com.de

Warum muss glibcs ​​strlen so kompliziert sein, um schnell zu laufen?

Ich habe den strlen Code hier durchgesehen und mich gefragt, ob die im Code verwendeten Optimierungen wirklich benötigt werden. Warum funktioniert so etwas zum Beispiel nicht gleich gut oder besser?

unsigned long strlen(char s[]) {
    unsigned long i;
    for (i = 0; s[i] != '\0'; i++)
        continue;
    return i;
}

Ist einfacherer Code für den Compiler nicht besser und/oder einfacher zu optimieren?

Der Code von strlen auf der Seite hinter dem Link sieht folgendermaßen aus:

/* Copyright (C) 1991, 1993, 1997, 2000, 2003 Free Software Foundation, Inc.
   This file is part of the GNU C Library.
   Written by Torbjorn Granlund ([email protected]),
   with help from Dan Sahlin ([email protected]);
   commentary by Jim Blandy ([email protected]).

   The GNU C Library is free software; you can redistribute it and/or
   modify it under the terms of the GNU Lesser General Public
   License as published by the Free Software Foundation; either
   version 2.1 of the License, or (at your option) any later version.

   The GNU C Library is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   Lesser General Public License for more details.

   You should have received a copy of the GNU Lesser General Public
   License along with the GNU C Library; if not, write to the Free
   Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
   02111-1307 USA.  */

#include <string.h>
#include <stdlib.h>

#undef strlen

/* Return the length of the null-terminated string STR.  Scan for
   the null terminator quickly by testing four bytes at a time.  */
size_t
strlen (str)
     const char *str;
{
  const char *char_ptr;
  const unsigned long int *longword_ptr;
  unsigned long int longword, magic_bits, himagic, lomagic;

  /* Handle the first few characters by reading one character at a time.
     Do this until CHAR_PTR is aligned on a longword boundary.  */
  for (char_ptr = str; ((unsigned long int) char_ptr
            & (sizeof (longword) - 1)) != 0;
       ++char_ptr)
    if (*char_ptr == '\0')
      return char_ptr - str;

  /* All these elucidatory comments refer to 4-byte longwords,
     but the theory applies equally well to 8-byte longwords.  */

  longword_ptr = (unsigned long int *) char_ptr;

  /* Bits 31, 24, 16, and 8 of this number are zero.  Call these bits
     the "holes."  Note that there is a hole just to the left of
     each byte, with an extra at the end:

     bits:  01111110 11111110 11111110 11111111
     bytes: AAAAAAAA BBBBBBBB CCCCCCCC DDDDDDDD

     The 1-bits make sure that carries propagate to the next 0-bit.
     The 0-bits provide holes for carries to fall into.  */
  magic_bits = 0x7efefeffL;
  himagic = 0x80808080L;
  lomagic = 0x01010101L;
  if (sizeof (longword) > 4)
    {
      /* 64-bit version of the magic.  */
      /* Do the shift in two steps to avoid a warning if long has 32 bits.  */
      magic_bits = ((0x7efefefeL << 16) << 16) | 0xfefefeffL;
      himagic = ((himagic << 16) << 16) | himagic;
      lomagic = ((lomagic << 16) << 16) | lomagic;
    }
  if (sizeof (longword) > 8)
    abort ();

  /* Instead of the traditional loop which tests each character,
     we will test a longword at a time.  The tricky part is testing
     if *any of the four* bytes in the longword in question are zero.  */
  for (;;)
    {
      /* We tentatively exit the loop if adding MAGIC_BITS to
     LONGWORD fails to change any of the hole bits of LONGWORD.

     1) Is this safe?  Will it catch all the zero bytes?
     Suppose there is a byte with all zeros.  Any carry bits
     propagating from its left will fall into the hole at its
     least significant bit and stop.  Since there will be no
     carry from its most significant bit, the LSB of the
     byte to the left will be unchanged, and the zero will be
     detected.

     2) Is this worthwhile?  Will it ignore everything except
     zero bytes?  Suppose every byte of LONGWORD has a bit set
     somewhere.  There will be a carry into bit 8.  If bit 8
     is set, this will carry into bit 16.  If bit 8 is clear,
     one of bits 9-15 must be set, so there will be a carry
     into bit 16.  Similarly, there will be a carry into bit
     24.  If one of bits 24-30 is set, there will be a carry
     into bit 31, so all of the hole bits will be changed.

     The one misfire occurs when bits 24-30 are clear and bit
     31 is set; in this case, the hole at bit 31 is not
     changed.  If we had access to the processor carry flag,
     we could close this loophole by putting the fourth hole
     at bit 32!

     So it ignores everything except 128's, when they're aligned
     properly.  */

      longword = *longword_ptr++;

      if (
#if 0
      /* Add MAGIC_BITS to LONGWORD.  */
      (((longword + magic_bits)

        /* Set those bits that were unchanged by the addition.  */
        ^ ~longword)

       /* Look at only the hole bits.  If any of the hole bits
          are unchanged, most likely one of the bytes was a
          zero.  */
       & ~magic_bits)
#else
      ((longword - lomagic) & himagic)
#endif
      != 0)
    {
      /* Which of the bytes was the zero?  If none of them were, it was
         a misfire; continue the search.  */

      const char *cp = (const char *) (longword_ptr - 1);

      if (cp[0] == 0)
        return cp - str;
      if (cp[1] == 0)
        return cp - str + 1;
      if (cp[2] == 0)
        return cp - str + 2;
      if (cp[3] == 0)
        return cp - str + 3;
      if (sizeof (longword) > 4)
        {
          if (cp[4] == 0)
        return cp - str + 4;
          if (cp[5] == 0)
        return cp - str + 5;
          if (cp[6] == 0)
        return cp - str + 6;
          if (cp[7] == 0)
        return cp - str + 7;
        }
    }
    }
}
libc_hidden_builtin_def (strlen)

Warum läuft diese Version schnell?

Macht es nicht viel unnötige Arbeit?

286
user11954200

Sienichtbrauchen und Siesollten niemalssolchen Code schreiben - besonders wenn Sie kein C-Compiler/Standardbibliotheksanbieter sind. Es ist Code, der verwendet wird, um strlen mit einigen sehr fragwürdigen Speed-Hacks und Annahmen zu implementieren (die nicht mit Behauptungen getestet oder in den Kommentaren erwähnt werden):

  • _unsigned long_ besteht entweder aus 4 oder 8 Bytes
  • bytes sind 8 Bits
  • ein Zeiger kann auf _unsigned long long_ und nicht auf _uintptr_t_ gesetzt werden
  • man kann den Zeiger einfach ausrichten, indem man prüft, ob die 2 oder 3 Bits niedrigster Ordnung Null sind
  • man kann auf eine Zeichenfolge als _unsigned long_ s zugreifen
  • man kann über das Ende des Arrays hinaus ohne negative Auswirkungen lesen.

Darüber hinaus könnte ein guter Compiler sogar Code ersetzen, der als geschrieben wurde

_size_t stupid_strlen(const char s[]) {
    size_t i;
    for (i=0; s[i] != '\0'; i++)
        ;
    return i;
}
_

(Beachten Sie, dass es sich um einen Typ handeln muss, der mit _size_t_ kompatibel ist.) Mit einer Inline-Version des in strlen integrierten Compilers oder Vektorisieren des Codes. Es ist jedoch unwahrscheinlich, dass ein Compiler die komplexe Version optimieren kann.


Die Funktion strlen wird durch C11 7.24.6. beschrieben als:

Beschreibung

  1. Die Funktion strlen berechnet die Länge der Zeichenfolge, auf die s zeigt.

Gibtzurück

  1. Die Funktion strlen gibt die Anzahl der Zeichen vor dem abschließenden Nullzeichen zurück.

Wenn sich die Zeichenfolge, auf die s zeigt, in einem Array von Zeichen befindet, das gerade lang genug ist, um die Zeichenfolge und die abschließende NUL zu enthalten, istbehaviourundefiniertwenn wir auf den String nach dem Nullterminator zugreifen, zum Beispiel in

_char *str = "hello world";  // or
char array[] = "hello world";
_

Also ist der nur Weg in vollständig portablem/standardkonformem C zur Implementierung dieseskorrektso, wie er in IhrerFragegeschrieben ist. mit Ausnahme von trivialen Transformationen - Sie können so tun, als wären Sie schneller, indem Sie die Schleife usw. abrollen, aber es muss immer noch ein Byte auf einmal ausgeführt werden.

(Wie Kommentatoren hervorgehoben haben, ist es nicht immer schlecht, vernünftige oder bekanntermaßen sichere Annahmen zu nutzen, wenn eine strikte Portabilität zu belastend ist. Insbesondere bei Code, der Teil von einer bestimmten C-Implementierung ist. Aber Sie müssen die Regeln verstehen, bevor Sie wissen, wie/wann Sie sie biegen können.)


Die verknüpfte strlen -Implementierung überprüft zuerst die Bytes einzeln, bis der Zeiger auf die natürliche 4- oder 8-Byte-Ausrichtungsgrenze des _unsigned long_ zeigt. Der C-Standard besagt, dass der Zugriff auf einen Zeiger, der nicht richtig ausgerichtet ist,undefiniertes Verhaltenhat. Dies muss also unbedingt getan werden, damit der nächste schmutzige Trick noch schmutziger wird. (In der Praxis wird auf einer anderen CPU-Architektur als x86 eine falsch ausgerichtete Word- oder Doppelwortlast fehlerhaft sein. C ist nicht eine tragbare Assemblersprache, aber dieser Code verwendet sie auf diese Weise.) Dies ermöglicht es auch, über das Ende eines Objekts hinaus zu lesen, ohne dass bei Implementierungen, bei denen der Speicherschutz in ausgerichteten Blöcken (z. B. virtuellen 4-KB-Speicherseiten) funktioniert, Fehler auftreten können.

Jetzt kommt der schmutzige Teil: Der Code bricht das Versprechen und liest 4 oder 8 8-Bit-Bytes gleichzeitig (a _long int_) und verwendet einen Bit-Trick mit vorzeichenloser Addition, um schnell herauszufinden Wenn sich in diesen 4 oder 8 Bytes any zero Bytes befinden, wird eine speziell gestaltete Zahl verwendet, die dazu führt, dass das Übertragsbit Bits ändert, die von einer Bitmaske abgefangen werden. Im Wesentlichen würde dies dann herausfinden, ob eines der 4 oder 8 Bytes in der Maske angeblich Nullen sindschnellerals das Durchlaufen jedes dieser Bytes. Schließlich gibt es am Ende eine Schleife, um welches Byte die erste Null war, falls vorhanden, und um das Ergebnis zurückzugeben.

Das größte Problem ist, dass in sizeof (unsigned long) - 1 Zeiten außerhalb von sizeof (unsigned long) Fällen über das Ende der Zeichenfolge hinaus gelesen wird - nur wenn sich das Nullbyte im last Zugriff befindet Byte (dh im Little-Endian das höchstwertige und im Big-Endian das niedrigstwertige), greift es nicht außerhalb der Grenzen auf das Array zu!


Der Code ist bad code, obwohl er zum Implementieren von strlen in einer C-Standardbibliothek verwendet wird. Es enthält mehrere implementierungsdefinierte und undefinierte Aspekte und sollte nichtirgendwoanstelle des vom System bereitgestellten strlen verwendet werden. Ich habe die Funktion in _the_strlen_ umbenannt hier und fügte das folgende main hinzu:

_int main(void) {
    char buf[12];
    printf("%zu\n", the_strlen(fgets(buf, 12, stdin)));
}
_

Der Puffer ist sorgfältig dimensioniert, sodass er genau die Zeichenfolge _hello world_ und den Terminator enthalten kann. Auf meinem 64-Bit-Prozessor beträgt _unsigned long_ jedoch 8 Byte, sodass der Zugriff auf den letzteren Teil diesen Puffer überschreiten würde.

Wenn ich jetzt mit _-fsanitize=undefined_ und _-fsanitize=address_ kompiliere und das resultierende Programm ausführe, erhalte ich:

_% ./a.out
hello world
=================================================================
==8355==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffffe63a3f8 at pc 0x55fbec46ab6c bp 0x7ffffe63a350 sp 0x7ffffe63a340
READ of size 8 at 0x7ffffe63a3f8 thread T0
    #0 0x55fbec46ab6b in the_strlen (.../a.out+0x1b6b)
    #1 0x55fbec46b139 in main (.../a.out+0x2139)
    #2 0x7f4f0848fb96 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x21b96)
    #3 0x55fbec46a949 in _start (.../a.out+0x1949)

Address 0x7ffffe63a3f8 is located in stack of thread T0 at offset 40 in frame
    #0 0x55fbec46b07c in main (.../a.out+0x207c)

  This frame has 1 object(s):
    [32, 44) 'buf' <== Memory access at offset 40 partially overflows this variable
HINT: this may be a false positive if your program uses some custom stack unwind mechanism or swapcontext
      (longjmp and C++ exceptions *are* supported)
SUMMARY: AddressSanitizer: stack-buffer-overflow (.../a.out+0x1b6b) in the_strlen
Shadow bytes around the buggy address:
  0x10007fcbf420: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x10007fcbf430: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x10007fcbf440: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x10007fcbf450: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x10007fcbf460: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x10007fcbf470: 00 00 00 00 00 00 00 00 00 00 f1 f1 f1 f1 00[04]
  0x10007fcbf480: f2 f2 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x10007fcbf490: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x10007fcbf4a0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x10007fcbf4b0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x10007fcbf4c0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07 
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
==8355==ABORTING
_

d.h. schlimme Dinge sind passiert.

232
Antti Haapala

Zusätzlich zu den großartigen Antworten hier möchte ich darauf hinweisen, dass der in der Frage verknüpfte Code für die Implementierung von strlen durch GNU bestimmt ist.

Die OpenBSD-Implementierung von strlen ist dem in der Frage vorgeschlagenen Code sehr ähnlich. Die Komplexität einer Implementierung wird vom Autor bestimmt.

...
#include <string.h>

size_t
strlen(const char *str)
{
    const char *s;

    for (s = str; *s; ++s)
        ;
    return (s - str);
}

DEF_STRONG(strlen);

[~ # ~] edit [~ # ~] : Der oben verlinkte OpenBSD-Code scheint eine Fallback-Implementierung für ISAs zu sein, die es dort nicht gibt eigene asm Implementierung. Je nach Architektur gibt es unterschiedliche Implementierungen von strlen. Der Code für AMD64 strlen lautet beispielsweise asm. Ähnlich wie in den Kommentaren von PeterCordes/ Antwort wird darauf hingewiesen, dass die Nicht-Fallback-Implementierungen GNU] ebenfalls asm sind.

39
Peschke

Kurz gesagt, dies ist eine Leistungsoptimierung, die die Standardbibliothek durchführen kann, indem sie weiß, mit welchem ​​Compiler sie kompiliert wird. Sie sollten keinen solchen Code schreiben, es sei denn, Sie schreiben eine Standardbibliothek und können von einem bestimmten Compiler abhängen. Insbesondere wird die Ausrichtungsanzahl von Bytes gleichzeitig verarbeitet - 4 auf 32-Bit-Plattformen, 8 auf 64-Bit-Plattformen. Dies bedeutet, dass es vier- oder achtmal schneller sein kann als eine naive Byteration.

Betrachten Sie das folgende Bild, um zu erklären, wie dies funktioniert. Nehmen Sie hier die 32-Bit-Plattform an (4-Byte-Ausrichtung).

(

Nehmen wir an, der Buchstabe "H" von "Hallo Welt!" Zeichenfolge wurde als Argument für strlen angegeben. Da die CPU es mag, Dinge im Speicher auszurichten (idealerweise address % sizeof(size_t) == 0), werden die Bytes vor der Ausrichtung byteweise mit der langsamen Methode verarbeitet.

Dann wird für jeden Block mit Ausrichtungsgröße durch Berechnen von (longbits - 0x01010101) & 0x80808080 != 0 Überprüft, ob eines der Bytes innerhalb einer Ganzzahl Null ist. Diese Berechnung ist falsch positiv, wenn mindestens eines der Bytes höher als 0x80 Ist, aber meistens sollte es funktionieren. Ist dies nicht der Fall (wie im gelben Bereich), wird die Länge um die Ausrichtungsgröße erhöht.

Wenn sich herausstellt, dass eines der Bytes innerhalb einer Ganzzahl Null ist (oder 0x81), Wird die Zeichenfolge byteweise überprüft, um die Position von Null zu bestimmen.

Dies kann einen Zugriff außerhalb der Grenzen ermöglichen. Da er sich jedoch innerhalb einer Ausrichtung befindet, ist es mehr als wahrscheinlich, dass er in Ordnung ist. Speicherzuordnungseinheiten haben normalerweise keine Genauigkeit auf Byte-Ebene.

34
Konrad Borowski

Sie möchten, dass der Code korrekt, wartbar und schnell ist. Diese Faktoren haben unterschiedliche Bedeutung:

"richtig" ist absolut notwendig.

"wartbar" hängt davon ab, wie viel Sie den Code pflegen werden: strlen ist seit über 40 Jahren eine Standard-C-Bibliotheksfunktion. Es wird sich nicht ändern. Die Wartbarkeit ist daher für diese Funktion ziemlich unwichtig.

"Schnell": In vielen Anwendungen verbrauchen strcpy, strlen usw. einen erheblichen Teil der Ausführungszeit. Den gleichen Geschwindigkeitsgewinn wie diese komplizierte, aber nicht sehr komplizierte Implementierung von strlen durch Verbesserung des Compilers zu erzielen, würde heldenhafte Anstrengungen erfordern.

Schnell zu sein hat einen weiteren Vorteil: Wenn Programmierer herausfinden, dass das Aufrufen von "strlen" die schnellste Methode ist, mit der sie die Anzahl der Bytes in einer Zeichenfolge messen können, sind sie nicht mehr versucht, ihren eigenen Code zu schreiben, um die Dinge schneller zu machen.

Für strlen ist Geschwindigkeit viel wichtiger und Wartbarkeit viel weniger wichtig als für den meisten Code, den Sie jemals schreiben werden.

Warum muss es so kompliziert sein? Angenommen, Sie haben eine 1.000-Byte-Zeichenfolge. Die einfache Implementierung untersucht 1.000 Bytes. Eine aktuelle Implementierung würde wahrscheinlich 64-Bit-Wörter gleichzeitig untersuchen, was 125 64-Bit- oder 8-Byte-Wörter bedeutet. Es könnten sogar Vektoranweisungen verwendet werden, die beispielsweise 32 Bytes gleichzeitig untersuchen, was noch komplizierter und noch schneller wäre. Die Verwendung von Vektoranweisungen führt zu Code, der etwas komplizierter, aber recht unkompliziert ist. Um zu überprüfen, ob eines von acht Bytes in einem 64-Bit-Wort Null ist, sind einige clevere Tricks erforderlich. Für mittlere bis lange Zeichenfolgen ist zu erwarten, dass dieser Code etwa viermal schneller ist. Für eine so wichtige Funktion wie strlen lohnt es sich, eine komplexere Funktion zu schreiben.

PS. Der Code ist nicht sehr portabel. Es ist jedoch Teil der Standard C-Bibliothek, die Teil der Implementierung ist - es muss nicht portierbar sein.

PPS. Jemand hat ein Beispiel veröffentlicht, in dem sich ein Debugging-Tool über den Zugriff auf Bytes nach dem Ende einer Zeichenfolge beschwert hat. Es kann eine Implementierung entworfen werden, die Folgendes garantiert: Wenn p ein gültiger Zeiger auf ein Byte ist, gibt jeder Zugriff auf ein Byte in demselben ausgerichteten Block, der gemäß dem C-Standard ein undefiniertes Verhalten wäre, einen nicht angegebenen Wert zurück.

PPPS. Intel hat seinen späteren Prozessoren Anweisungen hinzugefügt, die einen Baustein für die Funktion strstr () bilden (Suchen eines Teilstrings in einem String). Ihre Beschreibung ist umwerfend, aber sie können diese bestimmte Funktion wahrscheinlich 100-mal schneller machen. (Wenn ein Array a "Hallo, Welt!" Und ein Array b mit 16 Bytes "HelloHelloHelloH" beginnt und mehr Bytes enthält, stellt sich heraus, dass die Zeichenfolge a in b nicht früher als ab Index 15 vorkommt.) .

32
gnasher729

Kurz gesagt: Das Überprüfen einer Zeichenfolge Byte für Byte ist bei Architekturen, die gleichzeitig größere Datenmengen abrufen können, möglicherweise langsam.

Wenn die Prüfung auf Nullbeendigung auf 32- oder 64-Bit-Basis durchgeführt werden kann, wird die Anzahl der vom Compiler durchzuführenden Prüfungen verringert. Dies versucht der verknüpfte Code unter Berücksichtigung eines bestimmten Systems. Sie machen Annahmen über Adressierung, Ausrichtung, Cache-Nutzung, nicht standardmäßige Compiler-Setups usw. usw.

Das Lesen von Byte für Byte wie in Ihrem Beispiel wäre ein sinnvoller Ansatz auf einer 8-Bit-CPU oder beim Schreiben einer tragbaren Bibliothek, die in Standard C geschrieben ist.

Es ist keine gute Idee, in C-Standardbibliotheken nach Ratschlägen zum Schreiben von schnellem/gutem Code zu suchen, da dieser nicht portierbar ist und auf nicht standardmäßigen Annahmen oder schlecht definiertem Verhalten beruht. Wenn Sie ein Anfänger sind, ist das Lesen eines solchen Codes wahrscheinlich schädlicher als das Lernen.

24
Lundin