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 main1
y 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 seq
o 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
Respuestas:
Copiado de: ¿Por qué la demora en el ciclo? a sus órdenes:
Puede acortar el caso de prueba a:
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 lobash
hace 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:
Ahí es donde pasa la lista de argumentos a las funciones, y esta vez el shell necesita copiar los valores (
bash
termina 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
bash
si desea comparar su propia compilación con la uno del sistema. Por ejemplo, Ubuntu usa--without-bash-malloc
)fuente
RELSTATUS=alpha
aRELSTATUS=release
en elconfigure
script.--without-bash-malloc
yRELSTATUS=release
para los resultados de la pregunta. Eso todavía muestra un problema con la llamada a f.:
y mejora un poco las llamadasf
. Mire los tiempos de test2 en la pregunta.