O Bash tem problemas de desempenho usando listas de argumentos?

11

Resolvido no bash 5.0

fundo

Para um histórico (e compreensão (e tentando evitar os votos negativos que essa pergunta parece atrair)), explicarei o caminho que me levou a esse problema (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 havendo mais de 1 milhão de caracteres Unicode, testar 20.000 deles não parece ser tanto assim.
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 a forma test1 "$@"ou similar. Agora eu percebo o quão ruim é essa ideia no bash.

Agora, suponha que seja necessário cronometrar (n = 1000) cada solução para descobrir qual é melhor; nessas condições, você terminará com uma estrutura semelhante 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, apenas para serem apresentadas aqui.
Os originais foram progressivamente aparados para descobrir onde estava o grande atraso.

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

No processo de simplificação para descobrir exatamente onde estava o atraso (e reduzir cada função de teste para quase nada é o extremo após muitas tentativas), decidi remover a passagem de argumentos para cada função de teste para descobrir quanto tempo melhorava. um fator de 6, não muito.

Para tentar você mesmo, remova toda a "$@"função in main1(ou faça uma cópia) e teste novamente (ou ambas main1e a cópia main2(com main2 "$@")) para comparar. Essa é a estrutura básica abaixo, no post original (OP).

Mas eu me perguntava: por que a concha está demorando tanto para "não fazer nada"? Sim, apenas "alguns segundos", mas ainda assim, por quê?

Isso me fez testar em outras conchas 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 nenhum argumento fica atrasado pelos argumentos no pai ( main#). Esta é a descrição a seguir e foi a postagem original (OP) abaixo.

Postagem original.

Chamar uma função (na liberação do Bash 4.4.12 (1)) para não fazer nada f1(){ :; }é mil vezes mais lento do que :mas apenas se houver argumentos definidos na função de chamada pai , Por que?

#!/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 entradas ou saídas usadas na função f1, o atraso de um fator de mil (1000) é inesperado. 1


Estendendo os testes a várias conchas, os resultados são consistentes, a maioria das conchas não apresenta problemas nem sofre 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 ......

Remova o comentário dos outros dois testes para confirmar que nenhum dos seqprocessos ou a lista de argumentos é a fonte do atraso.

1 Sabe-se que a passagem de resultados por argumentos aumentará o tempo de execução. Obrigado@slm

NotAnUnixNazi
fonte
3
Salvo pelo efeito meta. unix.meta.stackexchange.com/q/5021/3562
Joshua

Respostas:

9

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

Você pode reduzir o caso de teste para:

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

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

Meu palpite seria que o tempo é gasto economizando $@em uma pilha e restaurando-a posteriormente. Possivelmente bashfaz isso de maneira 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 reservatórios 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 dessa vez o shell precisa copiar os valores ( bashacaba sendo 5 vezes mais lento para esse).

(Inicialmente, pensei que era pior no bash 5 (atualmente em alfa), mas isso era devido à habilitação da depuração malloc nas versões de desenvolvimento, conforme observado pelo @egmont; verifique também como sua distribuição é criada bashse você quiser comparar sua própria versão com a do sistema. Por exemplo, o Ubuntu usa --without-bash-malloc)

Stéphane Chazelas
fonte
Como a depuração é removida?
NotAnUnixNazi
@isaac, eu fiz isso mudando RELSTATUS=alphapara RELSTATUS=releaseno configurescript.
Stéphane Chazelas
Resultados de teste adicionados para ambos --without-bash-malloce RELSTATUS=releasepara os resultados da pergunta. Isso ainda mostra um problema com a chamada para f.
NotAnUnixNazi
@ Isaac, sim, eu apenas disse que costumava estar errado ao dizer que era pior no bash5. Não é pior, é tão ruim quanto.
Stéphane Chazelas 12/08/18
Não, não é tão ruim . O Bash5 resolve o problema da chamada :e melhora um pouco a chamada f. Veja os horários do teste2 na pergunta.
precisa saber é o seguinte