Warum ist printf von Bash schneller als /usr/bin/printf?

Warum ist printf von Bash schneller als /usr/bin/printf?

Ich habe zwei Möglichkeiten, printfmein System anzurufen:

$ type -a printf
printf is a shell builtin
printf is /usr/bin/printf
$ file /usr/bin/printf
/usr/bin/printf: ELF 64-bit LSB  executable, x86-64, version 1 (SYSV), dynamically
linked (uses shared libs), for GNU/Linux 2.6.32,
BuildID[sha1]=d663d220e5c2a2fc57462668d84d2f72d0563c33, stripped

Das eine ist also ein integriertes Bash-Programm und das andere eine ordnungsgemäß kompilierte ausführbare Datei. Ich hätte ein Programm erwartet, dessen einzige Aufgabe darin besteht, printfviel schneller zu sein als das integrierte Shell-Programm. Zugegeben, das integrierte Programm ist bereits in den Speicher geladen, aber die tatsächliche Ausführungszeit sollte in einem dedizierten Programm schneller sein, oder? Es wäre optimiert, um eine Sache im Sinne der besten Unix-Philosophie sehr gut zu machen.

Scheinbar nicht:

$ >/tmp/foo; time for i in `seq 1 3000`; do printf '%s ' "$i" >> /tmp/foo; done;
real    0m0.065s
user    0m0.036s
sys     0m0.024s

$ >/tmp/foo; time for i in `seq 1 3000`; do /usr/bin/printf '%s ' "$i" >> /tmp/foo; done;   
real    0m18.097s
user    0m1.048s
sys     0m7.124s

Wie @Guru anmerkt, liegt ein Großteil davon an den Kosten für die Erstellung von Threads, die nur durch entstehen /usr/bin/printf. Wenn das alles wäre, würde ich erwarten, dass die ausführbare Datei schneller ist als die integrierte, wenn sie außerhalb einer Schleife ausgeführt wird. Leider /usr/bin/printfist die Größe einer Variablen, die es annehmen kann, begrenzt, sodass ich dies nur mit einer relativ kurzen Zeichenfolge testen konnte:

$ i=$(seq 1 28000 | awk '{k=k$1}END{print k}'); time /usr/bin/printf '%s ' "$i" > /dev/null; 

real    0m0.035s
user    0m0.004s
sys     0m0.028s

$ i=$(seq 1 28000 | awk '{k=k$1}END{print k}'); time printf '%s ' "$i" > /dev/null; 

real    0m0.008s
user    0m0.008s
sys     0m0.000s

Das eingebaute Programm ist immer noch durchweg und deutlich schneller. Um es noch deutlicher zu machen, lassen wir beide neue Prozesse starten:

$ time for i in `seq 1 1000`; do /usr/bin/printf '%s ' "$i" >/dev/null; done;   
real    0m33.695s
user    0m0.636s
sys     0m30.628s

$ time for i in `seq 1 1000`; do bash -c "printf '%s ' $i" >/dev/null; done;   

real    0m3.557s
user    0m0.380s
sys     0m0.508s

Der einzige Grund, der mir einfällt, ist, dass die zu druckende Variable intern ist bashund direkt an das integrierte Element übergeben werden kann. Reicht das aus, um den Geschwindigkeitsunterschied zu erklären? Welche anderen Faktoren spielen eine Rolle?

Antwort1

Standalone-Druckf

Ein Teil des „Aufwands“ beim Aufrufen eines Prozesses besteht darin, dass mehrere ressourcenintensive Dinge geschehen müssen.

  1. Die ausführbare Datei muss von der Festplatte geladen werden. Dies führt zu Verzögerungen, da auf die Festplatte zugegriffen werden muss, um den Binärblob von der Festplatte zu laden, auf der die ausführbare Datei gespeichert ist.
  2. Die ausführbare Datei wird normalerweise mit dynamischen Bibliotheken erstellt, daher müssen auch einige sekundäre Dateien der ausführbaren Datei geladen werden (d. h. es werden weitere binäre Blob-Daten von der Festplatte gelesen).
  3. Overhead des Betriebssystems. Jeder Prozess, den Sie aufrufen, verursacht Overhead in Form einer Prozess-ID, die dafür erstellt werden muss. Außerdem muss Speicherplatz geschaffen werden, um sowohl die in den Schritten 1 und 2 von der Festplatte geladenen Binärdaten unterzubringen, als auch mehrere Strukturen, die gefüllt werden müssen, um Dinge wie die Umgebung der Prozesse (Umgebungsvariablen usw.) zu speichern.

Auszug aus einem Strang von/usr/bin/printf

    $ strace /usr/bin/printf "%s\n" "hello world"
    *execve("/usr/bin/printf", ["/usr/bin/printf", "%s\\n", "hello world"], [/* 91 vars */]) = 0
    brk(0)                                  = 0xe91000
    mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fd155a6b000
    access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
    open("/etc/ld.so.cache", O_RDONLY)      = 3
    fstat(3, {st_mode=S_IFREG|0644, st_size=242452, ...}) = 0
    mmap(NULL, 242452, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fd155a2f000
    close(3)                                = 0
    open("/lib64/libc.so.6", O_RDONLY)      = 3
    read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0p\357!\3474\0\0\0"..., 832) = 832
    fstat(3, {st_mode=S_IFREG|0755, st_size=1956608, ...}) = 0
    mmap(0x34e7200000, 3781816, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x34e7200000
    mprotect(0x34e7391000, 2097152, PROT_NONE) = 0
    mmap(0x34e7591000, 20480, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x191000) = 0x34e7591000
    mmap(0x34e7596000, 21688, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x34e7596000
    close(3)                                = 0
    mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fd155a2e000
    mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fd155a2c000
    arch_prctl(ARCH_SET_FS, 0x7fd155a2c720) = 0
    mprotect(0x34e7591000, 16384, PROT_READ) = 0
    mprotect(0x34e701e000, 4096, PROT_READ) = 0
    munmap(0x7fd155a2f000, 242452)          = 0
    brk(0)                                  = 0xe91000
    brk(0xeb2000)                           = 0xeb2000
    brk(0)                                  = 0xeb2000
    open("/usr/lib/locale/locale-archive", O_RDONLY) = 3
    fstat(3, {st_mode=S_IFREG|0644, st_size=99158752, ...}) = 0
    mmap(NULL, 99158752, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fd14fb9b000
    close(3)                                = 0
    fstat(1, {st_mode=S_IFIFO|0600, st_size=0, ...}) = 0
    mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fd155a6a000
    write(1, "hello world\n", 12hello world
    )           = 12
    close(1)                                = 0
    munmap(0x7fd155a6a000, 4096)            = 0
    close(2)                                = 0
    exit_group(0)                           = ?*

Wenn Sie sich das Obige ansehen, bekommen Sie eine Vorstellung von den zusätzlichen Ressourcen, die /usr/bin/printfdadurch aufgewendet werden müssen, dass es sich um eine eigenständige ausführbare Datei handelt.

Eingebautes printf

Mit der erstellten Version printfwurden alle Bibliotheken, von denen sie abhängt, sowie ihr Binärblob bereits beim Aufruf von Bash in den Speicher geladen. Nichts davon muss also erneut ausgeführt werden.

Wenn Sie die integrierten „Befehle“ an Bash senden, führen Sie im Grunde genommen einen Funktionsaufruf aus, da bereits alles geladen wurde.

Eine Analogie

Wenn Sie schon einmal mit einer Programmiersprache wie Perl gearbeitet haben, entspricht dies dem Aufrufen der Funktion ( system("mycmd")) oder der Verwendung der Backticks ( `mycmd`). Wenn Sie eine dieser beiden Aktionen ausführen, erstellen Sie einen separaten Prozess mit eigenem Overhead, anstatt die Funktionen zu verwenden, die Ihnen durch die Kernfunktionen von Perl angeboten werden.

Anatomie des Linux-Prozessmanagements

Es gibt einen ziemlich guten Artikel auf IBM Developerworks, der die verschiedenen Aspekte der Erstellung und Zerstörung von Linux-Prozessen sowie die verschiedenen am Prozess beteiligten C-Bibliotheken aufschlüsselt. Der Artikel trägt den Titel:Anatomie der Linux-Prozessverwaltung - Erstellung, Verwaltung, Planung und LöschungEs ist auch erhältlich alsPDF.

Antwort2

Die Ausführung eines externen Befehls /usr/bin/printfführt zur Erstellung eines Prozesses, was bei einer integrierten Shell nicht der Fall ist. Bei einer Schleife von 3000 werden also 3000 Prozesse erstellt, was die Ausführung verlangsamt.

Sie können dies überprüfen, indem Sie sie außerhalb einer Schleife ausführen:

Antwort3

Während die Tatsache, dass die Zeit zum Erzeugen und Einrichten eines neuen Prozesses und zum Laden, Ausführen und Initialisieren, Bereinigen und Beenden eines Programms und seiner Bibliotheksabhängigkeiten darin die tatsächlich benötigte Zeit für die Ausführung der Aktion bei weitem übersteigt, bereits behandelt wurde, sind hier einige Zeitangaben mit verschiedenen printfImplementierungen für eine teure Aktion, dienichtvom Rest in den Schatten gestellt:

$ time /usr/bin/printf %2000000000s > /dev/null
/usr/bin/printf %2000000000s > /dev/null  13.72s user 1.42s system 99% cpu 15.238 total

$ time busybox printf %2000000000s > /dev/null
busybox printf %2000000000s > /dev/null  1.50s user 0.49s system 95% cpu 2.078 total


$ time bash -c 'printf %2000000000s' > /dev/null
bash -c 'printf %2000000000s' > /dev/null  4.59s user 3.35s system 84% cpu 9.375 total

$ time zsh -c 'printf %2000000000s' > /dev/null
zsh -c 'printf %2000000000s' > /dev/null  1.48s user 0.24s system 81% cpu 2.115 total

$ time ksh -c 'printf %2000000000s' > /dev/null
ksh -c 'printf %2000000000s' > /dev/null  0.48s user 0.00s system 88% cpu 0.543 total

$ time mksh -c 'printf %2000000000s' > /dev/null
mksh -c 'printf %2000000000s' > /dev/null  13.59s user 1.57s system 99% cpu 15.262 total

$ time ash -c 'printf %2000000000s' > /dev/null
ash -c 'printf %2000000000s' > /dev/null  13.74s user 1.42s system 99% cpu 15.214 total

$ time yash -c 'printf %2000000000s' > /dev/null
yash -c 'printf %2000000000s' > /dev/null  13.73s user 1.40s system 99% cpu 15.186 total

Sie sehen, dass GNU zumindest in dieser Hinsicht printfnicht auf Leistung optimiert wurde. Es macht printfsowieso nicht viel Sinn, einen Befehl wie zu optimieren, da bei 99,999 % der Verwendungen die für die Ausführung der Aktion benötigte Zeit ohnehin von der Ausführungszeit überschattet wird. Es ist viel sinnvoller, Befehle wie grepoder zu optimieren sed, die möglicherweise Gigabyte an Daten ineinslaufen.

verwandte Informationen