Por que abrir um arquivo mais rápido do que ler conteúdo variável?

36

Em um bashscript, preciso de vários valores dos /proc/arquivos. Até agora, tenho dezenas de linhas cumprindo os arquivos diretamente assim:

grep -oP '^MemFree: *\K[0-9]+' /proc/meminfo

Em um esforço para tornar isso mais eficiente, salvei o conteúdo do arquivo em uma variável e confirmei que:

a=$(</proc/meminfo)
echo "$a" | grep -oP '^MemFree: *\K[0-9]+'

Em vez de abrir o arquivo várias vezes, basta abri-lo uma vez e cumprimentar o conteúdo da variável, que presumi ser mais rápido - mas, na verdade, é mais lento:

bash 4.4.19 $ time for i in {1..1000};do grep ^MemFree /proc/meminfo;done >/dev/null
real    0m0.803s
user    0m0.619s
sys     0m0.232s
bash 4.4.19 $ a=$(</proc/meminfo)
bash 4.4.19 $ time for i in {1..1000};do echo "$a"|grep ^MemFree; done >/dev/null
real    0m1.182s
user    0m1.425s
sys     0m0.506s

O mesmo vale para dashe zsh. Suspeitei do estado especial dos /proc/arquivos como motivo, mas quando copio o conteúdo de /proc/meminfoum arquivo normal e uso os resultados são os mesmos:

bash 4.4.19 $ cat </proc/meminfo >meminfo
bash 4.4.19 $ time for i in $(seq 1 1000);do grep ^MemFree meminfo; done >/dev/null
real    0m0.790s
user    0m0.608s
sys     0m0.227s

O uso de uma string here para salvar o pipe o torna um pouco mais rápido, mas ainda não tão rápido quanto nos arquivos:

bash 4.4.19 $ time for i in $(seq 1 1000);do <<<"$a" grep ^MemFree; done >/dev/null
real    0m0.977s
user    0m0.758s
sys     0m0.268s

Por que abrir um arquivo mais rápido do que ler o mesmo conteúdo de uma variável?

sobremesa
fonte
@ l0b0 Esta suposição não é defeituosa, a pergunta mostra como eu a criei e as respostas explicam por que esse é o caso. Sua edição agora faz com que as respostas não respondam mais à pergunta do título: elas não dizem se é esse o caso.
dessert
OK, esclarecido. Como o cabeçalho estava errado na grande maioria dos casos, não apenas para determinados arquivos especiais mapeados na memória.
l0b0 21/02
@ l0b0 Não, isso é o que eu estou pedindo aqui: “Eu suspeitava que o estado especial de /proc/arquivos como uma razão, mas quando eu copiar o conteúdo de /proc/meminfoum arquivo regular e uso que os resultados são os mesmos:” É não especial para /proc/arquivos, a leitura de arquivos regulares também é mais rápida!
dessert

Respostas:

47

Aqui, não se trata de abrir um arquivo em vez de ler o conteúdo de uma variável, mas mais de criar um processo extra ou não.

grep -oP '^MemFree: *\K[0-9]+' /proc/meminfobifurca um processo que executa grepque é aberto /proc/meminfo(um arquivo virtual, na memória, sem E / S de disco envolvido) o lê e corresponde ao regexp.

A parte mais cara disso é bifurcar o processo e carregar o utilitário grep e suas dependências de biblioteca, fazer o vínculo dinâmico, abrir o banco de dados de localidade, dezenas de arquivos que estão no disco (mas provavelmente armazenados em cache na memória).

A parte sobre leitura /proc/meminfoé insignificante em comparação, o kernel precisa de pouco tempo para gerar as informações e grepprecisa de pouco tempo para lê-las.

Se você executar strace -cisso, verá que as chamadas de um open()e um read()sistemas usadas para ler /proc/meminfosão amendoins, em comparação com tudo o que grepfaz para iniciar ( strace -cnão conta a bifurcação).

Em:

a=$(</proc/meminfo)

Na maioria dos shells que suportam esse $(<...)operador ksh, o shell apenas abre o arquivo e lê seu conteúdo (e retira os caracteres de nova linha à direita). bashé diferente e muito menos eficiente, pois bifurca um processo para fazer essa leitura e transmite os dados ao pai por meio de um canal. Mas aqui, é feito uma vez, para que não importe.

Em:

printf '%s\n' "$a" | grep '^MemFree'

O shell precisa gerar dois processos, que estão sendo executados simultaneamente, mas interagem entre si por meio de um pipe. A criação, desmontagem e escrita e leitura do cano têm um pequeno custo. O custo muito maior é a desova de um processo extra. O agendamento dos processos também tem algum impacto.

Você pode achar que o uso do <<<operador zsh o torna um pouco mais rápido:

grep '^MemFree' <<< "$a"

No zsh e no bash, isso é feito escrevendo o conteúdo de $aum arquivo temporário, que é mais barato do que gerar um processo extra, mas provavelmente não trará nenhum ganho em comparação com a obtenção direta dos dados /proc/meminfo. Isso ainda é menos eficiente do que a sua abordagem que copia /proc/meminfono disco, pois a gravação do arquivo temporário é feita a cada iteração.

dashnão suporta strings here, mas seus heredocs são implementados com um pipe que não envolve gerar um processo extra. Em:

 grep '^MemFree' << EOF
 $a
 EOF

A concha cria um tubo, bifurca um processo. O filho executa grepcom seu stdin como a extremidade de leitura do canal, e o pai grava o conteúdo na outra extremidade do canal.

Mas é provável que esse manuseio de tubos e sincronização de processos seja mais caro do que apenas obter os dados imediatamente /proc/meminfo.

O conteúdo de /proc/meminfoé curto e não leva muito tempo para produzir. Se você quiser salvar alguns ciclos da CPU, remova as partes caras: processos de bifurcação e execução de comandos externos.

Gostar:

IFS= read -rd '' meminfo < /proc/meminfo
memfree=${meminfo#*MemFree:}
memfree=${memfree%%$'\n'*}
memfree=${memfree#"${memfree%%[! ]*}"}

Evite, bashporém, cuja correspondência de padrões seja muito ineficiente. Com zsh -o extendedglob, você pode reduzi-lo para:

memfree=${${"$(</proc/meminfo)"##*MemFree: #}%%$'\n'*}

Note que isso ^é especial em muitos shells (Bourne, fish, rc, es e zsh com a opção extendedglob pelo menos), eu recomendo citá-lo. Observe também que echonão pode ser usado para gerar dados arbitrários (daí o uso printfacima).

Stéphane Chazelas
fonte
4
No caso de printfvocê dizer que o shell precisa gerar dois processos, mas printfum shell não está embutido?
David Conrad
6
@DavidConrad Sim, mas a maioria dos shells não tenta analisar o pipeline para quais partes ele pode executar no processo atual. Apenas se bifurca e deixa as crianças descobrirem. Nesse caso, o processo pai bifurca-se duas vezes; a criança do lado esquerdo então vê um embutido e o executa; a criança do lado direito vê grepe executa.
chepner 20/02
1
@DavidConrad, o canal é um mecanismo IPC; portanto, em qualquer caso, os dois lados terão que executar em processos diferentes. Enquanto estiver dentro A | B, existem alguns shells, como o AT&T ksh ou zsh, que são executados Bno processo atual do shell, se for um comando integrado, composto ou de função, não conheço nenhum que seja executado Ano processo atual. De qualquer forma, para fazer isso, eles teriam que lidar com o SIGPIPE de maneira complexa, como se Aestivesse executando no processo filho e sem encerrar o shell para que o comportamento não seja muito surpreendente quando Bsair cedo. É muito mais fácil executar Bno processo pai.
Stéphane Chazelas
Bash suporta<<<
D. Ben Knoble
1
@ D.BenKnoble, eu não quis dizer bashque não apoiava <<<, apenas que o operador veio zshcomo $(<...)veio do ksh.
Stéphane Chazelas
6

No seu primeiro caso, você está apenas usando o utilitário grep e encontrando algo no arquivo /proc/meminfo. /procÉ um sistema de arquivos virtual, portanto o /proc/meminfoarquivo está na memória e requer muito pouco tempo para buscar seu conteúdo.

Mas, no segundo caso, você está criando um canal, passando a saída do primeiro comando para o segundo comando usando esse canal, o que é caro.

A diferença é devido a /proc(porque está na memória) e ao pipe, veja o exemplo abaixo:

time for i in {1..1000};do grep ^MemFree /proc/meminfo;done >/dev/null

real    0m0.914s
user    0m0.032s
sys     0m0.148s


cat /proc/meminfo > file
time for i in {1..1000};do grep ^MemFree file;done >/dev/null

real    0m0.938s
user    0m0.032s
sys     0m0.152s


time for i in {1..1000};do echo "$a"|grep ^MemFree; done >/dev/null

real    0m1.016s
user    0m0.040s
sys     0m0.232s
Prvt_Yadav
fonte
1

Você está chamando um comando externo nos dois casos (grep). A chamada externa requer um subshell. A bifurcação dessa concha é a causa fundamental do atraso. Ambos os casos são semelhantes, portanto: um atraso semelhante.

Se você quiser ler o arquivo externo apenas uma vez e usá-lo (de uma variável) várias vezes, não saia do shell:

meminfo=$(< /dev/meminfo)    
time for i in {1..1000};do 
    [[ $meminfo =~ MemFree:\ *([0-9]*)\ *.B ]] 
    printf '%s\n' "${BASH_REMATCH[1]}"
done

O que leva apenas cerca de 0,1 segundos, em vez de 1 segundo completo para a chamada grep.

Isaac
fonte