Bash ha problemi di prestazioni nell'uso degli elenchi di argomenti?


11

Risolto in bash 5.0

sfondo

Per lo sfondo (e comprensione (e cercando di evitare i voti negativi che questa domanda sembra attrarre)) spiegherò il percorso che mi ha portato a questo problema (beh, il migliore che posso ricordare due mesi dopo).

Supponiamo che tu stia facendo alcuni test di shell per un elenco di caratteri Unicode:

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

e ci sono più di 1 milione di caratteri Unicode, testarne 20.000 non sembra essere così tanto.
Supponi anche di impostare i caratteri come argomenti posizionali:

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

con l'intenzione di passare i personaggi a ciascuna funzione per elaborarli in modi diversi. Quindi le funzioni dovrebbero avere la forma test1 "$@"o simili. Ora mi rendo conto di quanto sia pessima questa idea a Bash.

Ora, supponiamo che ci sia la necessità di cronometrare (an = 1000) ogni soluzione per scoprire quale sia la migliore, in tali condizioni si finirà con una struttura simile 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 "$@"

Le funzioni test#sono rese molto semplici solo per essere presentate qui.
Gli originali furono progressivamente ritagliati per scoprire dov'era l'enorme ritardo.

Lo script sopra funziona, puoi eseguirlo e perdere qualche secondo facendo pochissimo.

Nel processo di semplificazione per trovare esattamente dove fosse il ritardo (e ridurre ogni funzione di test a quasi nulla è l'estremo dopo molte prove) ho deciso di rimuovere il passaggio degli argomenti a ciascuna funzione di test per scoprire quanto il tempo è migliorato, solo un fattore 6, non molto.

Per provare te stesso, rimuovi tutte le "$@"funzioni in main1(o crea una copia) e riprova (o entrambi main1e la copia main2(con main2 "$@")) per confrontare. Questa è la struttura di base in basso nel post originale (OP).

Ma mi chiedevo: perché la shell sta impiegando così tanto tempo a "non fare nulla"? Sì, solo "un paio di secondi", ma comunque, perché ?.

Questo mi ha fatto testare in altre shell per scoprire che solo Bash aveva questo problema.
Prova ksh ./script(lo stesso script di cui sopra).

Questo porta a questa descrizione: chiamare una funzione ( test#) senza alcun argomento viene ritardato dagli argomenti nel parent ( main#). Questa è la descrizione che segue ed era il post originale (OP) di seguito.

Posta originale

Chiamare una funzione (in Bash 4.4.12 (1)-release) per non fare nulla f1(){ :; }è mille volte più lento di :ma solo se ci sono argomenti definiti nella funzione di chiamata parent , Why?

#!/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

Risultati di 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

Non ci sono argomenti né input o output utilizzati nella funzione f1, il ritardo di un fattore di mille (1000) è imprevisto. 1


Estendendo i test a più shell, i risultati sono coerenti, la maggior parte delle shell non ha problemi né subisce ritardi (vengono utilizzati gli stessi n e m):

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

risultati:

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 ......

Sblocca gli altri due test per confermare che nessuno dei due seqo l'elaborazione dell'elenco argomenti sono la fonte del ritardo.

1 Ènoto che il passaggio dei risultati per argomenti aumenterà il tempo di esecuzione. Grazie@slm


3
Salvato dal meta effetto. unix.meta.stackexchange.com/q/5021/3562
Joshua

Risposte:


9

Copiato da: Perché il ritardo nel loop? a tua richiesta:

È possibile abbreviare il test case per:

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

Sta chiamando una funzione mentre $@è grande che sembra attivarla.

La mia ipotesi sarebbe che il tempo è trascorso salvando $@su uno stack e ripristinandolo in seguito. Forse lo bashfa in modo molto inefficiente duplicando tutti i valori o qualcosa del genere. Il tempo sembra essere in o (n²).

Ottieni lo stesso tipo di tempo in altre shell per:

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

È qui che passate l'elenco degli argomenti alle funzioni, e questa volta la shell deve copiare i valori ( bashfinisce per essere 5 volte più lento di quello).

(Inizialmente pensavo che fosse peggio in bash 5 (attualmente in alpha), ma questo era dovuto al debug malloc che era abilitato nelle versioni di sviluppo come notato da @egmont; controlla anche come si sviluppa la tua distribuzione bashse vuoi confrontare la tua build con il quello di sistema. Ad esempio, Ubuntu usa --without-bash-malloc)


Come viene rimosso il debug?
NotAnUnixNazi

@isaac, l'ho fatto cambiando RELSTATUS=alphain RELSTATUS=releasenella configuresceneggiatura.
Stéphane Chazelas,

Aggiunti i risultati dei test per entrambi --without-bash-malloce RELSTATUS=releaseper i risultati delle domande. Ciò mostra ancora un problema con la chiamata a f.
NotAnUnixNazi

@Isaac, sì, ho appena detto che ero sbagliato a dire che era peggio in bash5. Non è peggio, è altrettanto male.
Stéphane Chazelas,

No, non è così male . Bash5 risolve il problema con la chiamata :e migliora leggermente la chiamata f. Guarda i tempi di test2 nella domanda.
NotAnUnixNazi
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.