Bash tem problemas de desempenho usando listas de argumentos?

12

Antecedentes

Para o fundo (e compreensão (e tentar evitar as opiniões negativas que esta pergunta parece atrair)) Vou explicar o caminho que me levou a esta questão (bem, o melhor que me lembro dois meses depois).

Suponha que você esteja fazendo alguns testes de shell para uma lista de caracteres Unicode:

printf "$(printf '\U%x ' {33..200})"

e existindo mais de 1 milhão de caracteres Unicode, testar 20.000 deles não parece ser muito.
Suponha também que você defina os caracteres como argumentos posicionais:

set -- $(printf "$(printf '\U%x ' {33..20000})")

com a intenção de passar os caracteres para cada função para processá-los de maneiras diferentes. Portanto, as funções devem ter o formato test1 "$@" ou similar. Agora eu percebo como essa idéia é ruim no bash.

Agora, suponha que existe a necessidade de tempo (n = 1000) em cada solução para descobrir qual é o melhor, sob tais condições você terminará com uma estrutura similar a:

#!/bin/bash --
TIMEFORMAT='real: %R'  # '%R %U %S'

set -- $(printf "$(printf '\U%x ' {33..20000})")
n=1000

test1(){ echo "$1"; } >/dev/null
test2(){ echo "$#"; } >/dev/null
test3(){ :; }

main1(){ time for i in $(seq $n); do test1 "$@"; done
         time for i in $(seq $n); do test2 "$@"; done
         time for i in $(seq $n); do test3 "$@"; done
       }

main1 "$@"

As funções test# são muito simples para serem apresentadas aqui.
Os originais foram progressivamente reduzidos para descobrir onde estava o enorme atraso.

O script acima funciona, você pode executá-lo e perder alguns segundos fazendo muito pouco.

No processo de simplificar para encontrar exatamente onde o atraso foi (e reduzir cada função de teste a quase nada é o extremo depois de muitas tentativas), decidi remover a passagem de argumentos para cada função de teste para descobrir quanto tempo melhorado, apenas um fator de 6, não muito.

Para experimentar, remova todo o "$@" na função main1 (ou faça uma cópia) e teste novamente (ou ambos main1 e a cópia main2 (com main2 "$@" )) para comparar. Esta é a estrutura básica abaixo no post original (OP).

Mas eu me perguntava: por que a casca demorou tanto para "não fazer nada" ?. Sim, apenas "alguns segundos", mas ainda assim, por que?.

Isso me fez testar em outros shells para descobrir que apenas o bash tinha esse problema.
Tente ksh ./script (o mesmo script acima).

Isso leva a esta descrição: chamar uma função ( test# ) sem que nenhum argumento seja atrasado pelos argumentos no pai ( main# ). Esta é a descrição que segue e foi o post original (OP) abaixo.

postagem original.

Chamar uma função (no Bash 4.4.12 (1) -release) para não fazer nada f1(){ :; } é mil vezes mais lenta que : mas somente se houver argumentos definidos no < função de chamada strong> parent , por quê?

#!/bin/bash
TIMEFORMAT='real: %R'

f1   () { :; }

f2   () {
   echo "                     args = $#";
   printf '1 function no   args yes '; time for ((i=1;i<$n;i++)); do  :   ; done 
   printf '2 function yes  args yes '; time for ((i=1;i<$n;i++)); do  f1  ; done
   set --
   printf '3 function yes  args no  '; time for ((i=1;i<$n;i++)); do  f1  ; done
   echo
        }

main1() { set -- $(seq $m)
          f2  ""
          f2 "$@"
        }

n=1000; m=20000; main1

Resultados de test1 :

                     args = 1
1 function no   args yes real:  0.013
2 function yes  args yes real:  0.024
3 function yes  args no  real:  0.020

                     args = 20000
1 function no   args yes real:  0.010
2 function yes  args yes real: 20.326
3 function yes  args no  real:  0.019

Não há argumentos nem entrada nem saída usados na função f1 , o atraso de um fator de mil (1000) é inesperado. 1

Estendendo os testes para vários shells, os resultados são consistentes, a maioria dos shells não tem problemas nem sofrem atrasos (os mesmos n e m são usados):

test2(){
          for sh in dash mksh ksh zsh bash b50sh
      do
          echo "$sh" >&2
#         \time -f '\t%E' seq "$m" >/dev/null
#         \time -f '\t%E' "$sh" -c 'set -- $(seq '"$m"'); for i do :; done'
          \time -f '\t%E' "$sh" -c 'f(){ :;}; while [ "$((i+=1))" -lt '"$n"' ]; do : ; done;' $(seq $m)
          \time -f '\t%E' "$sh" -c 'f(){ :;}; while [ "$((i+=1))" -lt '"$n"' ]; do f ; done;' $(seq $m)
      done
}

test2

Resultados:

dash
        0:00.01
        0:00.01
mksh
        0:00.01
        0:00.02
ksh
        0:00.01
        0:00.02
zsh
        0:00.02
        0:00.04
bash
        0:10.71
        0:30.03
b55sh             # --without-bash-malloc
        0:00.04
        0:17.11
b56sh             # RELSTATUS=release
        0:00.03
        0:15.47
b50sh             # Debug enabled (RELSTATUS=alpha)
        0:04.62
        xxxxxxx    More than a day ......

Descomente os outros dois testes para confirmar que nem seq ou o processamento da lista de argumentos é a origem do atraso.

1 sabe-se que passar resultados por argumentos aumentará o tempo de execução . Obrigado @slm

    
por Isaac 12.08.2018 / 09:15

1 resposta

9

Copiado de: Por que o atraso no loop? a seu pedido:

Você pode encurtar o caso de teste para:

time bash -c 'f(){ :;};for i do f; done' {0..10000}

Ele está chamando uma função enquanto $@ é grande e parece ativá-la.

Meu palpite seria que o tempo gasto economizando $@ em uma pilha e restaurando-a depois. Possivelmente bash faz isso de maneira muito ineficiente duplicando todos os valores ou algo assim. O tempo parece estar em o (n²).

Você obtém o mesmo tipo de tempo em outros shells para:

time zsh -c 'f(){ :;};for i do f "$@"; done' {0..10000}

É aí que você passa a lista de argumentos para as funções, e desta vez o shell precisa para copiar os valores ( bash acaba sendo 5 vezes mais lento para aquele).

(inicialmente achei que era pior no bash 5 (atualmente em alpha), mas isso foi devido à depuração do malloc sendo ativada nas versões de desenvolvimento conforme observado por @egmont; também verifique como sua distribuição constrói bash se você quer comparar sua própria build com a do sistema, por exemplo, o Ubuntu usa --without-bash-malloc )

    
por 12.08.2018 / 10:12

Tags