Bash tiene problemas de rendimiento al usar listas de argumentos?

11

Resuelto en bash 5.0

Antecedentes

Para conocer los antecedentes (y la comprensión (y tratar de evitar los votos negativos que esta pregunta parece atraer)) explicaré el camino que me llevó a este problema (bueno, lo mejor que puedo recordar dos meses después).

Suponga que está haciendo algunas pruebas de shell para una lista de caracteres Unicode:

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

y habiendo más de 1 millón de caracteres Unicode, probar 20,000 de ellos no parece ser tanto.
También suponga que establece los caracteres como argumentos posicionales:

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

con la intención de pasar los caracteres a cada función para procesarlos de diferentes maneras. Entonces las funciones deben tener la forma test1 "$@"o similar. Ahora me doy cuenta de lo mala que es esto en bash.

Ahora, suponga que existe la necesidad de cronometrar (an = 1000) cada solución para descubrir cuál es mejor, en tales condiciones terminará con una estructura 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 "$@"

Las funciones test#se hacen muy muy simples para ser presentadas aquí.
Los originales se recortaron progresivamente para encontrar dónde estaba el gran retraso.

El script anterior funciona, puedes ejecutarlo y perder unos segundos haciendo muy poco.

En el proceso de simplificar para encontrar exactamente dónde estaba el retraso (y reducir cada función de prueba a casi nada es el extremo después de muchas pruebas) decidí eliminar el paso de argumentos a cada función de prueba para averiguar cuánto mejoró el tiempo, solo un factor de 6, no mucho.

Para probarlo, elimine toda la "$@"función in main1(o haga una copia) y pruebe nuevamente (o ambos main1y la copia main2(con main2 "$@")) para comparar. Esta es la estructura básica a continuación en la publicación original (OP).

Pero me preguntaba: ¿por qué el caparazón tarda tanto en "no hacer nada"? Sí, solo "un par de segundos", pero aún así, ¿por qué?

Esto me hizo probar en otros shells para descubrir que solo bash tenía este problema.
Prueba ksh ./script(el mismo script que el anterior).

Esto lleva a esta descripción: llamar a una función ( test#) sin ningún argumento se retrasa por los argumentos en el padre ( main#). Esta es la descripción que sigue y fue la publicación original (OP) a continuación.

Publicación original

Llamar a una función (en Bash 4.4.12 (1) -release) para que no haga nada f1(){ :; }es mil veces más lento que, :pero solo si hay argumentos definidos en la función de llamada principal , ¿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

No hay argumentos ni entradas o salidas utilizadas en la función f1, el retraso de un factor de mil (1000) es inesperado. 1


Extendiendo las pruebas a varios shells, los resultados son consistentes, la mayoría de los shells no tienen problemas ni sufren retrasos (se utilizan los mismos nym):

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 las otras dos pruebas para confirmar que ninguno de los dos seqo el procesamiento de la lista de argumentos es la fuente del retraso.

1 Sesabe que pasar resultados por argumentos aumentará el tiempo de ejecución. Gracias@slm

NotAnUnixNazi
fuente
3
Salvado por el efecto meta. unix.meta.stackexchange.com/q/5021/3562
Joshua

Respuestas:

9

Copiado de: ¿Por qué la demora en el ciclo? a sus órdenes:

Puede acortar el caso de prueba a:

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

Llama a una función mientras $@es grande que parece desencadenarla.

Supongo que pasaría el tiempo ahorrando $@en una pila y restaurando después. Posiblemente lo bashhace de manera muy ineficiente duplicando todos los valores o algo así. El tiempo parece estar en o (n²).

Obtiene el mismo tipo de tiempo en otros depósitos para:

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

Ahí es donde pasa la lista de argumentos a las funciones, y esta vez el shell necesita copiar los valores ( bashtermina siendo 5 veces más lento para ese).

(Inicialmente pensé que era peor en bash 5 (actualmente en alfa), pero eso se debió a que la depuración de malloc se habilitó en las versiones de desarrollo como lo señaló @egmont; también verifique cómo se compila su distribución bashsi desea comparar su propia compilación con la uno del sistema. Por ejemplo, Ubuntu usa --without-bash-malloc)

Stéphane Chazelas
fuente
¿Cómo se elimina la depuración?
NotAnUnixNazi
@isaac, lo hice cambiando RELSTATUS=alphaa RELSTATUS=releaseen el configurescript.
Stéphane Chazelas
Se agregaron resultados de prueba para ambos --without-bash-mallocy RELSTATUS=releasepara los resultados de la pregunta. Eso todavía muestra un problema con la llamada a f.
NotAnUnixNazi
@ Isaac, sí, acabo de decir que solía estar equivocado al decir que fue peor en bash5. No es peor, es igual de malo.
Stéphane Chazelas
No, no es tan malo . Bash5 resuelve el problema con las llamadas :y mejora un poco las llamadas f. Mire los tiempos de test2 en la pregunta.
NotAnUnixNazi