Tenho duas maneiras de chamar printf
meu 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
Portanto, um é um bash integrado e o outro é um executável compilado adequado. Eu esperava um programa cujo único trabalho fosse printf
ser muito mais rápido que o shell embutido. É verdade que o built-in já está carregado na memória, mas o tempo real de execução deve ser mais rápido em um programa dedicado, certo? Seria otimizado para fazer algo muito bem no melhor da filosofia Unix.
Aparentemente não:
$ >/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
Muito disso, como aponta @Guru, se deve ao custo de criação de threads que só é incorrido por /usr/bin/printf
. Se isso fosse tudo, eu esperaria que o executável fosse mais rápido que o interno se fosse executado fora de um loop. Infelizmente, /usr/bin/printf
tem um limite para o tamanho de uma variável que pode assumir, então só pude testar isso com uma string relativamente curta:
$ 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
O integrado ainda é consistente e significativamente mais rápido. Para deixar ainda mais claro, vamos fazer com que ambos iniciem novos processos:
$ 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
A única razão que consigo pensar é que a variável que está sendo impressa é interna bash
e pode ser passada diretamente para o builtin. Isso é suficiente para explicar a diferença de velocidade? Que outros fatores estão em jogo?
Responder1
Impressão independente
Parte da "despesa" de invocar um processo é que várias coisas precisam acontecer e exigem muitos recursos.
- O executável deve ser carregado do disco, o que gera lentidão, pois o HDD foi acessado para carregar o blob binário do disco no qual o executável está armazenado.
- O executável normalmente é construído usando bibliotecas dinâmicas, portanto, alguns arquivos secundários do executável também terão que ser carregados (ou seja, mais dados de blob binários sendo lidos do HDD).
- Sobrecarga do sistema operacional. Cada processo invocado incorre em sobrecarga na forma de um ID de processo que precisa ser criado para ele. O espaço na memória também terá que ser reservado para abrigar os dados binários que estão sendo carregados do HDD nas etapas 1 e 2, bem como múltiplas estruturas que precisam ser preenchidas para armazenar coisas como o ambiente dos processos (variáveis de ambiente, etc.)
trecho de um trecho 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) = ?*
Olhando acima, você pode ter uma noção dos recursos adicionais que /usr/bin/printf
precisam ser incorridos por ser um executável independente.
Impressão integrada
Com a versão construída de printf
todas as bibliotecas das quais ela depende, bem como seu blob binário já foram carregados na memória quando o Bash foi invocado. Portanto, nada disso precisa ser incorrido novamente.
Efetivamente, quando você chama os "comandos" internos do Bash, você está realmente fazendo o que equivale a uma chamada de função, já que tudo já foi carregado.
Uma analogia
Se você já trabalhou com uma linguagem de programação, como Perl, é equivalente a fazer chamadas para a função ( system("mycmd")
) ou usar crases ( `mycmd`
). Ao fazer qualquer uma dessas coisas, você está criando um processo separado com sua própria sobrecarga, em vez de usar as funções que são oferecidas a você por meio das funções principais do Perl.
Anatomia do gerenciamento de processos Linux
Há um artigo muito bom no IBM Developerworks que detalha os vários aspectos de como os processos Linux são criados e destruídos junto com as diferentes bibliotecas C envolvidas no processo. O artigo é intitulado:Anatomia do gerenciamento de processos Linux - Criação, gerenciamento, agendamento e destruição. Também está disponível como umPDF.
Responder2
A execução de um comando externo /usr/bin/printf
leva à criação de um processo que um shell integrado não faz. Portanto, para um loop de 3.000, 3.000 processos criados e, portanto, mais lentos.
Você pode verificar isso executando-os fora de um loop:
Responder3
Embora o fato de que o tempo de geração e configuração de um novo processo e de carregamento, execução e inicialização, limpeza e encerramento de um programa e suas dependências de biblioteca nele ofusque em muito o tempo real necessário para executar a ação já tenha sido abordado, aqui existem alguns horários com printf
implementações diferentes para uma ação cara que énãoofuscado pelo 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
Você pode ver que, pelo menos nesse aspecto, o GNU printf
não foi otimizado para desempenho. De qualquer forma , não faz muito sentido otimizar um comando printf
porque, para 99,999% dos usos, o tempo gasto na execução da ação será ofuscado pelo tempo de execução de qualquer maneira. Faz muito mais sentido otimizar comandos como grep
ou sed
que podem potencialmente processar gigabytes de dados emumcorrer.