Ich habe zwei Möglichkeiten, printf
mein 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, printf
viel 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/printf
ist 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 bash
und 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.
- 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.
- 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).
- 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/printf
dadurch aufgewendet werden müssen, dass es sich um eine eigenständige ausführbare Datei handelt.
Eingebautes printf
Mit der erstellten Version printf
wurden 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/printf
fü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 printf
Implementierungen 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 printf
nicht auf Leistung optimiert wurde. Es macht printf
sowieso 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 grep
oder zu optimieren sed
, die möglicherweise Gigabyte an Daten ineinslaufen.