Estou tentando entender alguns problemas de desempenho relacionados a sed
e awk
, e fiz o seguinte experimento,
$ seq 100000 > test
$ yes 'NR==100001{print}' | head -n 5000 > test.awk
$ yes '100001{p;b}' | head -n 5000 > test.sed
$ time sed -nf test.sed test
real 0m3.436s
user 0m3.428s
sys 0m0.004s
$ time awk -F@ -f test.awk test
real 0m11.615s
user 0m11.582s
sys 0m0.007s
$ sed --version
sed (GNU sed) 4.5
$ awk --version
GNU Awk 4.2.1, API: 2.0 (GNU MPFR 3.1.6-p2, GNU MP 6.1.2)
Aqui, como o arquivo de teste contém apenas 100.000 linhas, todos os comandos test.sed
e test.awk
são no-ops. Ambos os programas só precisam combinar o número da linha com o endereço (in sed
) ou NR
(in awk
) para decidir que o comando não precisa ser executado, mas ainda há uma grande diferença no custo de tempo. Por que é o caso? Existe alguém com versões diferentes sed
e awk
instaladas que dê um resultado diferente neste teste?
Edit : Os resultados para mawk
(como sugerido por @mosvy), original-awk
(o nome para "one true awk" em sistemas baseados em debian, sugeridos por @GregA.Woods) e perl
são fornecidos abaixo,
$ time mawk -F@ -f test.awk test
real 0m5.934s
user 0m5.919s
sys 0m0.004s
$ time original-awk -F@ -f test.awk test
real 0m8.132s
user 0m8.128s
sys 0m0.004s
$ yes 'print if $.==100001;' | head -n 5000 > test.pl
$ time perl -n test.pl test
real 0m33.245s
user 0m33.110s
sys 0m0.019s
$ mawk -W version
mawk 1.3.4 20171017
$ perl --version
This is perl 5, version 28, subversion 1 (v5.28.1) built for x86_64-linux-thread-multi
A substituição -F@
por -F ''
não faz alterações observáveis no caso de gawk
e mawk
. original-awk
não suporta vazio FS
.
Edit 2
O teste de @mosvy dá resultados diferentes, 21s para sed
e 11s para mawk
, veja o comentário abaixo para mais detalhes.
awk
tem um conjunto de recursos mais amplo do quesed
, com uma sintaxe mais flexível. Portanto, não é irracional que demore mais para analisar seus scripts e executá-los.Como seu comando de exemplo (a parte dentro das chaves) nunca é executado, a parte sensível ao tempo deve ser sua expressão de teste.
awk
Primeiro, veja o teste no
awk
exemplo:e veja os efeitos disso em
gprof
(GNU awk 4.0.1):~50% do tempo é gasto em "interpretar", o loop de nível superior para executar os opcodes resultantes do script analisado.
Toda vez que o teste é executado (ou seja, 5.000 linhas de script * 100.000 linhas de entrada),
awk
deve-se:update_NR
).mk_number
).cmp_nodes
,cmp_scalar
,eval_condition
).free_wstr
,unref
)Outras
awk
implementações não terão exatamente o mesmo fluxo de chamadas, mas ainda terão que recuperar variáveis, converter automaticamente e comparar.sed
Em comparação, em
sed
, o "teste" é muito mais limitado. Pode ser apenas um único endereço, um intervalo de endereços ou nada (quando o comando é a primeira coisa na linha), esed
pode dizer desde o primeiro caractere se é um endereço ou comando. No exemplo é...um único endereço numérico. O perfil (GNU sed 4.2.2) mostra
Novamente, cerca de 50% do tempo está no nível superior
execute_program
. Nesse caso, é chamado uma vez por linha de entrada e, em seguida, faz um loop sobre os comandos analisados. O loop começa com uma verificação de endereço, mas isso não é tudo o que ele faz no seu exemplo (veja mais adiante).Os números de linha no script de entrada foram analisados em tempo de compilação (
in_integer
). Isso só precisa ser feito uma vez para cada número de endereço na entrada, ou seja. 5000 vezes e não contribui significativamente para o tempo de execução geral.Isso significa que a verificação de endereço,
match_address_p
, compara apenas inteiros que já estão disponíveis (através de structs e ponteiros).sed
melhorias adicionaisO perfil mostra que
match_address_p
é chamado 2*5000*100000 vezes, ou seja. duas vezes por linha de script*linha de entrada. Isso ocorre porque, nos bastidores, o GNUsed
trata o comando "start block"como uma ramificação negada até o final do bloco
Essa correspondência de endereço é bem- sucedida em todas as linhas de entrada, causando um desvio para o final do bloco (
}
). Esse final de bloco não tem endereço associado, portanto, é outra correspondência bem-sucedida. Isso explica por que tanto tempo é gasto emexecute_program
.Portanto, essa
sed
expressão seria ainda mais rápida se omitisse o não utilizado;b
, e o resultante desnecessário{...}
, deixando apenas o100001p
.Isso reduz pela metade o número de
match_address_p
chamadas e também reduz a maior parte do tempo gastoexecute_program
(porque a correspondência de endereço nunca é bem-sucedida).Na verdade, o script acima não é um noop para awk:
Mesmo que não utilize o conteúdo dos campos, de acordo com o manual do GAWK para cada registro que for lido, os seguintes passos são inevitavelmente executados:
Se você não estiver usando essas informações, elas serão descartadas posteriormente.
Se um separador de campo não ocorrer dentro do registro, o awk ainda terá que atribuir o texto a $0 (e no seu caso também a $1) e definir NF para o número real de campos obtidos (1 no exemplo acima)