Tengo dos formas de llamar printf
a mi sistema:
$ 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
Entonces, uno es un bash incorporado y el otro es un ejecutable compilado adecuado. Habría esperado un programa cuyo único trabajo fuera printf
mucho más rápido que el shell integrado. Por supuesto, la función incorporada ya está cargada en la memoria, pero el tiempo de ejecución real debería ser más rápido en un programa dedicado, ¿verdad? Estaría optimizado para hacer algo muy bien con lo mejor de la filosofía Unix.
Aparentemente no:
$ >/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
Mucho de esto, como señala @Guru, se debe al costo de crear subprocesos en el que solo incurre /usr/bin/printf
. Si eso fuera todo, esperaría que el ejecutable fuera más rápido que el integrado si se ejecuta fuera de un bucle. Desafortunadamente, /usr/bin/printf
tiene un límite en el tamaño de una variable que puede tomar, por lo que solo pude probar esto con una cadena relativamente corta:
$ 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
La versión integrada sigue siendo constante y significativamente más rápida. Para hacerlo aún más claro, hagamos que ambos inicien nuevos procesos:
$ 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 única razón que se me ocurre es que la variable que se está imprimiendo es interna bash
y se puede pasar directamente a la función incorporada. ¿Es eso suficiente para explicar la diferencia de velocidad? ¿Qué otros factores están en juego?
Respuesta1
Impresión independiente
Parte del "gasto" de invocar un proceso es que tienen que suceder varias cosas que requieren muchos recursos.
- El ejecutable debe cargarse desde el disco, esto genera lentitud ya que se debe acceder al disco duro para cargar el blob binario desde el disco en el que está almacenado el ejecutable.
- El ejecutable normalmente se crea utilizando bibliotecas dinámicas, por lo que también será necesario cargar algunos archivos secundarios del ejecutable (es decir, se leen más datos de blobs binarios desde el disco duro).
- Sobrecarga del sistema operativo. Cada proceso que invoca genera una sobrecarga en forma de un ID de proceso que debe crearse para él. También se habrá creado espacio en la memoria para albergar los datos binarios que se cargan desde el disco duro en los pasos 1 y 2, así como para llenar múltiples estructuras para almacenar cosas como el entorno de los procesos (variables de entorno, etc.)
extracto de un fragmento 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) = ?*
Al observar lo anterior, puede tener una idea de los recursos adicionales en los que /usr/bin/printf
se debe incurrir debido a que es un ejecutable independiente.
Impresión incorporada
Con la versión integrada, printf
todas las bibliotecas de las que depende, así como su blob binario, ya se cargaron en la memoria cuando se invocó Bash. Así que no hay que volver a incurrir en nada de eso.
Efectivamente, cuando llamas a los "comandos" integrados de Bash, en realidad estás haciendo lo que equivale a una llamada de función, ya que todo ya ha sido cargado.
Una analogía
Si alguna vez has trabajado con un lenguaje de programación, como Perl, es equivalente a realizar llamadas a la función ( system("mycmd")
) o usar las comillas invertidas ( `mycmd`
). Cuando haces cualquiera de esas cosas, estás bifurcando un proceso separado con su propia sobrecarga, en lugar de usar las funciones que se te ofrecen a través de las funciones principales de Perl.
Anatomía de la gestión de procesos de Linux
Hay un artículo bastante bueno sobre IBM Developerworks que desglosa los diversos aspectos de cómo se crean y destruyen los procesos de Linux junto con las diferentes bibliotecas C involucradas en el proceso. El artículo se titula:Anatomía de la gestión de procesos de Linux: creación, gestión, programación y destrucción. También está disponible comoPDF.
Respuesta2
La ejecución de un comando externo /usr/bin/printf
conduce a la creación de un proceso que un shell integrado no hace. Entonces, para un ciclo de 3000, se crean 3000 procesos y, por lo tanto, son más lentos.
Puedes comprobar esto ejecutándolos fuera de un bucle:
Respuesta3
Si bien ya se ha cubierto el hecho de que el tiempo de generación y configuración de un nuevo proceso y de carga, ejecución e inicialización, limpieza y finalización de un programa y sus dependencias de biblioteca eclipsa por mucho el tiempo realmente necesario para realizar la acción, aquí Hay algunos tiempos con diferentes printf
implementaciones para una acción costosa que esnoeclipsado por el resto:
$ 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
Puedes ver que al menos en ese sentido, GNU printf
no ha sido optimizado para el rendimiento. De todos modos , no tiene mucho sentido optimizar un comando printf
porque, para el 99,999% de los usos, el tiempo dedicado a realizar la acción se verá eclipsado por el tiempo de ejecución de todos modos. Tiene mucho más sentido optimizar comandos como grep
o sed
que potencialmente pueden procesar gigabytes de datos enunocorrer.