Pourquoi l'printingf de bash est-elle plus rapide que / usr / bin / printf?

J'ai deux façons d'appeler printf sur mon système:

 $ 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, ssortingpped 

Donc, l'un est un bash embedded et l'autre est un exécutable compilé. Je m'attendais à un programme dont le seul travail printf soit beaucoup plus rapide que le shell embedded. Certes, le builtin est déjà chargé en memory mais le time d'exécution réel devrait être plus rapide dans un programme dédié non? Il serait optimisé de faire une chose très bien dans le meilleur de la philosophie Unix.

Apparemment non:

 $ >/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 

Beaucoup de ceci, comme @Guru le souligne, est dû au coût de création de threads qui n'est encouru que par /usr/bin/printf . Si tout cela était, je m'attendrais à ce que l'exécutable soit plus rapide que le builtin s'il est exécuté en dehors d'une boucle. Malheureusement, /usr/bin/printf a une limite à la taille d'une variable qu'il peut prendre, donc je ne pouvais le tester qu'avec une string relativement courte:

 $ 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 

Le builtin est toujours constamment et significativement plus rapide. Pour le rendre encore plus clair, faisons en sorte que les deux commencent de nouveaux process:

 $ 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 

La seule raison pour laquelle je peux penser est que la variable imprimée est interne à bash et peut être transmise directement à l'embedded. Est-ce suffisant pour expliquer la différence de vitesse? Quels autres facteurs sont en jeu?

Impression autonome

Une partie de la «dépense» dans l'invocation d'un process est que plusieurs choses doivent se produire qui exigent beaucoup de ressources.

  1. L'exécutable doit être chargé à partir du disque, cela entraîne des lenteurs puisque le disque dur doit être accédé afin de charger le blob binary à partir du disque sur lequel l'exécutable est stocké.
  2. L'exécutable est généralement construit à l'aide de bibliothèques dynamics, de sorte que certains files secondaires de l'exécutable doivent également être chargés (c'est-à-dire que davantage de données blob binarys sont lues sur le disque dur).
  3. Système d'exploitation. Chaque process que vous invoque engendre des frais généraux sous la forme d'un ID de process qui doit être créé pour cela. De l'espace en memory sera également installé pour héberger datatables binarys chargées à partir du disque dur aux étapes 1 et 2, ainsi que de multiples structures pour stocker des éléments tels que l'environnement des process (variables d'environnement, etc.)

extrait d'une séquence de /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) = ?* 

En regardant à travers ce qui précède, vous pouvez avoir une idée des ressources supplémentaires que /usr/bin/printf doit supporter, car il s'agit d'un exécutable autonome.

Builtin printf

Avec la version construite d' printf toutes les bibliothèques dont elle dépend ainsi que son blob binary ont déjà été chargés en memory lorsque Bash a été appelé. Donc, rien de tout cela ne doit être encouru à nouveau.

Effectivement quand vous appelez les "commands" embeddedes à Bash, vous faites vraiment ce qui revient à un appel de fonction, puisque tout a déjà été chargé.

Une analogie

Si vous avez déjà travaillé avec un langage de programmation, comme Perl, cela équivaut à faire des appels à la fonction ( system("mycmd") ) ou à utiliser les backticks ( `mycmd` ). Lorsque vous faites l'une ou l'autre de ces choses, vous créez un process distinct avec ses propres tâches, par opposition aux fonctions qui vous sont offertes à travers les fonctions principales de Perl.

Anatomie de la gestion des process Linux

Il y a un très bon article sur IBM Developerworks qui décompose les différents aspects de la création et de la destruction des process Linux avec les différentes bibliothèques C impliquées dans le process. L'article s'intitule: Anatomie de la gestion des process Linux – Création, gestion, ordonnancement et destruction . Il est également disponible en format PDF .

L'exécution d'une command externe /usr/bin/printf conduit à une création de process que ne possède pas un shell embedded. Donc, pour une boucle de 3000, 3000 process créés, et donc plus lents.

Vous pouvez vérifier cela en les exécutant en dehors d'une boucle:

Bien que le time de frai et de mise en place d'un nouveau process, le chargement, l'exécution et l'initialisation, le nettoyage et la fermeture d'un programme et ses dependencies de bibliothèques couvrent de loin le time réellement nécessaire pour effectuer l'action. sont des timings avec différentes implémentations printf pour une action coûteuse qui n'est pas éclipsée par le rest:

 $ 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 

Vous pouvez voir qu'au less à cet égard, GNU printf n'a pas été optimisé pour les performances. De toute façon, il n'y a pas grand chose à optimiser une command comme printf car pour 99,999% des utilisations, le time passé à effectuer l'action va être éclipsé par le time d'exécution de toute façon. Il est beaucoup plus logique d'optimiser des commands comme grep ou sed qui peuvent potentiellement traiter des gigaoctets de données en une seule fois.