Warum ist sscanf von glibc erheblich langsamer als fscanf unter Linux?

Lesezeit: 4 Minuten

Benutzer-Avatar
Kerrek SB

Ich verwende GCC 4.8 und glibc 2.19 auf einem x86_64-Linux.

Beim Spielen mit verschiedenen Eingabemethoden für eine andere Frage habe ich verglichen fscanf und sscanf. Insbesondere würde ich entweder verwenden fscanf auf der Standardeingabe direkt:

char s[128]; int n;

while (fscanf(stdin, "%127s %d", s, &n) == 2) { }

Oder ich würde zuerst die gesamte Eingabe in einen Puffer einlesen und dann den Puffer mit durchlaufen sscanf. (Alles in den Puffer einzulesen dauert ein wenig.)

char s[128]; int n;
char const * p = my_data;

for (int b; sscanf(p, "%127s %d%n", s, &n, &b) == 2; p += b) { }

Zu meiner Überraschung, die fscanf Fassung ist erheblich Schneller. Beispielsweise werden mehrere zehntausend Zeilen mit verarbeitet fscanf dauert so lange:

10000       0.003927487 seconds time elapsed
20000       0.006860206 seconds time elapsed
30000       0.007933329 seconds time elapsed
40000       0.012881912 seconds time elapsed
50000       0.013516816 seconds time elapsed
60000       0.015670432 seconds time elapsed
70000       0.017393129 seconds time elapsed
80000       0.019837480 seconds time elapsed
90000       0.023925753 seconds time elapsed

Jetzt das gleiche mit sscanf:

10000       0.035864643 seconds time elapsed
20000       0.127150772 seconds time elapsed
30000       0.319828373 seconds time elapsed
40000       0.611551668 seconds time elapsed
50000       0.919187459 seconds time elapsed
60000       1.327831544 seconds time elapsed
70000       1.809843039 seconds time elapsed
80000       2.354809588 seconds time elapsed
90000       2.970678416 seconds time elapsed

Ich habe die Google Perf-Tools verwendet, um dies zu messen. Zum Beispiel für 50000 Zeilen, die fscanf Code erfordert etwa 50 Millionen Zyklen, und die sscanf Code über 3300 Millionen Zyklen. Also habe ich die Top-Call-Sites mit aufgeschlüsselt perf record/perf report. Mit fscanf:

 35.26%  xf  libc-2.19.so         [.] _IO_vfscanf
 23.91%  xf  [kernel.kallsyms]    [k] 0xffffffff8104f45a
  8.93%  xf  libc-2.19.so         [.] _int_malloc

Und mit sscanf:

 98.22%  xs  libc-2.19.so         [.] rawmemchr
  0.68%  xs  libc-2.19.so         [.] _IO_vfscanf
  0.38%  xs  [kernel.kallsyms]    [k] 0xffffffff8104f45a

Also fast immer mit sscanf darin verbracht wird rawmemchr! Warum ist das? Wie kann die fscanf Code diese Kosten vermeiden?

Ich habe versucht, danach zu suchen, aber das Beste, was ich finden konnte, ist diese Diskussion von gesperrt realloc Anrufe, die meiner Meinung nach hier nicht zutreffen. Das habe ich mir auch gedacht fscanf hat eine bessere Speicherlokalität (immer wieder denselben Puffer verwenden), aber das kann keinen so großen Unterschied machen.

Hat jemand irgendwelche Einblicke in diese seltsame Diskrepanz?

  • Vollständiger Code für: fscanf, sscanf

    – Kerrek SB

    29. Mai 2014 um 0:27 Uhr

  • Ich habe Probleme, den Quellcode für zu finden _IO_vfscanf. Dies ist das Beste, was ich finden konnte, aber das ist nicht unbedingt glibc 2.19.

    – Kerrek SB

    29. Mai 2014 um 0:31 Uhr

  • Zeigen Sie die Schleifenverarbeitung – es sieht so aus, als hätten Sie eine “Schlemiel der Maler”-Problem.

    – Michael Burr

    29. Mai 2014 um 0:32 Uhr

  • @MichaelBurr: Ich habe den Testcode verlinkt und die Schleifen in der Frage gepostet. Denkst du sscanf scannt jedes Mal bis zum Ende der Zeichenfolge? Das würde dem gespeicherten Wert widersprechen bdie den erwarteten Wert hat (dh bei jedem Aufruf wird eine Eingabezeile verbraucht).

    – Kerrek SB

    29. Mai 2014 um 0:33 Uhr


  • @MichaelBurr: Eigentlich denke ich, dass Michael Burr Recht hat, so scheint es sscanf sucht die gesamte Datei für die nachgestellte Null und parsen dann die drei gewünschten Variablen. Sehen Sie sich das Beispiel an linux.die.net/man/3/rawmemchr

    – Muhende Ente

    29. Mai 2014 um 0:49 Uhr


Benutzer-Avatar
Nr

sscanf() wandelt die übergebene Zeichenfolge in eine um _IO_FILE* um den String wie eine “Datei” aussehen zu lassen. Daher kann dasselbe interne _IO_vfscanf() sowohl für einen String als auch für eine FILE* verwendet werden.

Als Teil dieser Konvertierung, die in einer _IO_str_init_static_internal()-Funktion durchgeführt wird, wird sie jedoch aufgerufen __rawmemchr (ptr, '\0'); im Wesentlichen ein strlen()-Aufruf für Ihre Eingabezeichenfolge. Diese Konvertierung wird bei jedem Aufruf von sscanf() durchgeführt, und da Ihr Eingabepuffer ziemlich groß ist, wird er ziemlich viel Zeit damit verbringen, die Länge der Eingabezeichenfolge zu berechnen.

Das Erstellen einer DATEI* aus der Eingabezeichenfolge mit fmemopen() und die Verwendung von fscanf() könnte eine weitere Alternative sein.

  • Ich würde vorschlagen, einen Fehlerbericht gegen glibc einzureichen. Dieses Problem könnte im Prinzip definitiv behoben werden, indem man das Virtuelle macht FILE zur Verfügung gestellt von sscanf Verwenden Sie benutzerdefinierte Operationen, die keine Vorkenntnisse über die Zeichenfolgenlänge erfordern. Tatsächlich vermeidet unsere Implementierung in musl libc das Problem, also weiß ich, dass es möglich ist. 🙂

    – R.. GitHub HÖR AUF, EIS ZU HELFEN

    29. Mai 2014 um 2:55 Uhr

  • @R..: Ich hatte noch nie zuvor von Musl gehört – danke für den Hinweis!

    – Kerrek SB

    29. Mai 2014 um 19:32 Uhr

Es sieht aus wie das von glibc sscanf() scannt die Quellzeichenfolge nach der Länge, bevor irgendetwas anderes getan wird.

sscanf() (in stdio-common/sscanf.c) ist im Wesentlichen ein Wrapper um einen Aufruf an _IO_vsscanf() (in libio/iovsscanf.c). Und eines der ersten Dinge, die _IO_vsscanf() initialisiert sich selbst _IO_strfile Struktur durch Aufrufen _IO_str_init_static_internal() (in libio/strops.c), die die Länge der Zeichenfolge berechnet, wenn sie nicht angegeben wird.

1382470cookie-checkWarum ist sscanf von glibc erheblich langsamer als fscanf unter Linux?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy