¿La tubería, el desplazamiento o la expansión de parámetros son más eficientes?

26

Estoy tratando de encontrar la forma más eficiente de iterar a través de ciertos valores que son un número constante de valores separados en una lista de palabras separadas por espacios (no quiero usar una matriz). Por ejemplo,

list="1 ant bat 5 cat dingo 6 emu fish 9 gecko hare 15 i j"

Por lo tanto, quiero poder iterar por la lista y solo acceder a 1,5,6,9 y 15.

EDITAR: Debería haber dejado en claro que los valores que estoy tratando de obtener de la lista no tienen que ser diferentes en formato al resto de la lista. Lo que los hace especiales es únicamente su posición en la lista (en este caso, posición 1,4,7 ...). Así que la lista podría ser,1 2 3 5 9 8 6 90 84 9 3 2 15 75 55pero todavía quiero los mismos números. Y también, quiero poder hacerlo asumiendo que no sé la longitud de la lista.

Los métodos que he pensado hasta ahora son:

Método 1

set $list
found=false
find=9
count=1
while [ $count -lt $# ]; do
    if [ "${@:count:1}" -eq $find ]; then
    found=true
    break
    fi
    count=`expr $count + 3`
done

Método 2

set list
found=false
find=9
while [ $# ne 0 ]; do
    if [ $1 -eq $find ]; then
    found=true
    break
    fi
    shift 3
done

Método 3 Estoy bastante seguro de que la tubería hace que esta sea la peor opción, pero estaba tratando de encontrar un método que no use set, por curiosidad.

found=false
find=9
count=1
num=`echo $list | cut -d ' ' -f$count`
while [ -n "$num" ]; do
    if [ $num -eq $find ]; then
    found=true
    break
    fi
    count=`expr $count + 3`
    num=`echo $list | cut -d ' ' -f$count`
done

Entonces, ¿qué sería más eficiente, o me falta un método más simple?

Levi Uzodike
fuente
10
En primer lugar, no usaría un script de shell si la eficiencia es una preocupación importante. ¿Qué tan grande es su lista que marca la diferencia?
Barmar
2
Sin hacer estadísticas sobre las instancias reales de su problema, no sabrá nada. Esto incluye comparar con "programar en awk", etc. Si las estadísticas son demasiado caras, entonces buscar la eficiencia probablemente no valga la pena.
David Tonhofer
2
Levi, ¿cuál es exactamente la forma "eficiente" en tu definición? ¿Quieres encontrar una forma más rápida de iterar?
Sergiy Kolodyazhnyy

Respuestas:

18

Bastante simple con awk. Esto le dará el valor de cada cuarto campo para la entrada de cualquier longitud:

$ awk -F' ' '{for( i=1;i<=NF;i+=3) { printf( "%s%s", $i, OFS ) }; printf( "\n" ) }' <<< $list
1 5 6 9 15

Esto funciona aprovechando las awkvariables integradas como NF(el número de campos en el registro), y haciendo algunos forbucles simples para iterar a lo largo de los campos para darle las que desea sin necesidad de saber de antemano cuántos habrá.

O, si realmente desea esos campos específicos como se especifica en su ejemplo:

$ awk -F' ' '{ print $1, $4, $7, $10, $13 }' <<< $list
1 5 6 9 15

En cuanto a la pregunta sobre la eficiencia, la ruta más simple sería probar este o cada uno de sus otros métodos y usarlo timepara mostrar cuánto tiempo lleva; También puede usar herramientas como stracepara ver cómo fluyen las llamadas del sistema. Uso de timelooks como:

$ time ./script.sh

real    0m0.025s
user    0m0.004s
sys     0m0.008s

Puede comparar esa salida entre diferentes métodos para ver cuál es el más eficiente en términos de tiempo; Se pueden usar otras herramientas para otras métricas de eficiencia.

DopeGhoti
fuente
1
Buen punto, @MichaelHomer; He agregado un comentario aparte que aborda la cuestión de "cómo puedo determinar qué método es el más eficiente ".
DopeGhoti
2
@LeviUzodike Con respecto a echovs <<<, "idéntico" es una palabra demasiado fuerte. Se podría decir que stuff <<< "$list"es casi idéntico a printf "%s\n" "$list" | stuff. Con respecto a echovs printf, te dirijo a esta respuesta
JoL
55
@DopeGhoti En realidad lo hace. <<<agrega una nueva línea al final. Esto es similar a cómo $()elimina una nueva línea del final. Esto se debe a que las líneas están terminadas por nuevas líneas. <<<alimenta una expresión como una línea, por lo que debe terminar con una nueva línea. "$()"toma líneas y las proporciona como argumento, por lo que tiene sentido convertir eliminando la nueva línea de terminación.
JoL
3
@LeviUzodike awk es una herramienta muy poco apreciada. Hará que todo tipo de problemas aparentemente complejos sea fácil de resolver. Especialmente cuando intentas escribir una expresión regular compleja para algo como sed, a menudo puedes ahorrar horas escribiéndola procesalmente en awk. Aprenderlo pagará grandes dividendos.
Joe
1
@LeviUzodike: Sí, awkes un binario independiente que debe iniciarse. A diferencia de Perl o especialmente Python, el intérprete awk se inicia rápidamente (todavía todos los gastos generales habituales del enlazador dinámico de realizar bastantes llamadas al sistema, pero awk solo usa libc / libm y libdl, p. Ej., stracePara verificar las llamadas al sistema del inicio de awk) . Muchos shells (como bash) son bastante lentos, por lo que activar un proceso awk puede ser más rápido que recorrer los tokens en una lista con shell incorporado incluso para tamaños de lista pequeños. Y a veces puedes escribir un #!/usr/bin/awkguión en lugar de un #!/bin/shguión.
Peter Cordes
35
  • Primera regla de optimización de software: no lo haga .

    Hasta que sepa que la velocidad del programa es un problema, no hay necesidad de pensar qué tan rápido es. Si su lista tiene aproximadamente esa longitud o solo ~ 100-1000 elementos, probablemente ni siquiera se dará cuenta de cuánto tiempo lleva. Existe la posibilidad de que pase más tiempo pensando en la optimización que cuál sería la diferencia.

  • Segunda regla: medida .

    Esa es la forma segura de averiguarlo y la que da respuestas para su sistema. Especialmente con los proyectiles, hay tantos, y no todos son idénticos. Es posible que una respuesta para un shell no se aplique al tuyo.

    En programas más grandes, la creación de perfiles también va aquí. La parte más lenta podría no ser la que crees que es.

  • Tercero, la primera regla de optimización de script de shell: no use el shell .

    Si, en serio. Muchos shells no están hechos para ser rápidos (ya que el lanzamiento de programas externos no tiene que serlo), e incluso podrían analizar las líneas del código fuente cada vez.

    Use algo como awk o Perl en su lugar. En un trivial micro-benchmark que hice, awkfue docenas de veces más rápido que cualquier shell común al ejecutar un bucle simple (sin E / S).

    Sin embargo, si utiliza el shell, use las funciones integradas del shell en lugar de los comandos externos. Aquí, está utilizando lo exprque no está integrado en ningún caparazón que encontré en mi sistema, pero que se puede reemplazar con la expansión aritmética estándar. Por ejemplo, en i=$((i+1))lugar de i=$(expr $i + 1)incrementar i. Su uso de cuten el último ejemplo también podría reemplazarse con expansiones de parámetros estándar.

    Ver también: ¿Por qué usar un bucle de shell para procesar texto se considera una mala práctica?

Los pasos 1 y 2 deben aplicarse a su pregunta.

ilkkachu
fuente
12
# 0, cita tus expansiones :-)
Kusalananda
8
No es que los awkbucles sean necesariamente mejores o peores que los bucles de shell. Es que el shell es realmente bueno para ejecutar comandos y dirigir la entrada y salida hacia y desde los procesos, y francamente bastante torpe en todo lo demás; mientras que las herramientas como awkson fantásticas en el procesamiento de datos de texto, porque para eso awkestán hechos los shells y las herramientas (respectivamente) en primer lugar.
DopeGhoti
2
@DopeGhoti, sin embargo, los proyectiles parecen ser objetivamente más lentos. Algunos bucles while muy simples parecen ser> 25 veces más lentos dashque con gawk, y dashfue el shell más rápido que probé ...
ilkkachu
1
@ Joe, lo es :) dashy busyboxno es compatible (( .. )), creo que es una extensión no estándar. ++también se menciona explícitamente como no requerido, por lo que puedo decir, i=$((i+1))o : $(( i += 1))son los seguros.
ilkkachu
1
Re "más tiempo pensando" : esto descuida un factor importante. ¿Con qué frecuencia se ejecuta y para cuántos usuarios? Si un programa desperdicia 1 segundo, lo que podría ser arreglado por el programador al pensarlo durante 30 minutos, podría ser una pérdida de tiempo si solo hay un usuario que lo ejecutará una vez. Por otro lado, si hay un millón de usuarios, eso es un millón de segundos u 11 días de tiempo de usuario. Si el código desperdició un minuto de un millón de usuarios, eso es aproximadamente 2 años de tiempo de usuario.
agc
13

Solo voy a dar algunos consejos generales en esta respuesta, y no puntos de referencia. Los puntos de referencia son la única forma de responder de manera confiable las preguntas sobre el rendimiento. Pero dado que no dice cuántos datos está manipulando y con qué frecuencia realiza esta operación, no hay forma de hacer un punto de referencia útil. Lo que es más eficiente para 10 artículos y lo que es más eficiente para 1000000 artículos a menudo no es lo mismo.

Como regla general, invocar comandos externos es más costoso que hacer algo con construcciones de shell puro, siempre que el código de shell puro no implique un bucle. Por otro lado, un ciclo de shell que itera sobre una cadena grande o una gran cantidad de cadena probablemente sea más lento que una invocación de una herramienta de propósito especial. Por ejemplo, su invocación de bucle cutpodría ser notablemente lenta en la práctica, pero si encuentra una manera de hacer todo con una sola cutinvocación, es probable que sea más rápido que hacer lo mismo con la manipulación de cadenas en el shell.

Tenga en cuenta que el punto de corte puede variar mucho entre sistemas. Puede depender del kernel, de cómo está configurado el programador del kernel, del sistema de archivos que contiene los ejecutables externos, de la presión de CPU vs memoria que hay en este momento y muchos otros factores.

No llame exprpara realizar operaciones aritméticas si le preocupa el rendimiento. De hecho, no llame exprpara realizar operaciones aritméticas. Los proyectiles tienen aritmética incorporada, que es más clara y rápida que la invocación expr.

Parece que estás usando bash, ya que estás usando construcciones de bash que no existen en sh. Entonces, ¿por qué no usarías una matriz? Una matriz es la solución más natural, y es probable que también sea la más rápida. Tenga en cuenta que los índices de matriz comienzan en 0.

list=(1 2 3 5 9 8 6 90 84 9 3 2 15 75 55)
for ((count = 0; count += 3; count < ${#list[@]})); do
  echo "${list[$count]}"
done

Su script puede ser más rápido si usa sh, si su sistema tiene guión o ksh en shlugar de bash. Si usas sh, no obtienes matrices con nombre, pero aun así obtienes una matriz de parámetros posicionales, que puedes establecer con set. Para acceder a un elemento en una posición que no se conoce hasta el tiempo de ejecución, debe usarlo eval(¡tenga cuidado de citar las cosas correctamente!).

# List elements must not contain whitespace or ?*\[
list='1 2 3 5 9 8 6 90 84 9 3 2 15 75 55'
set $list
count=1
while [ $count -le $# ]; do
  eval "value=\${$count}"
  echo "$value"
  count=$((count+1))
done

Si solo desea acceder a la matriz una vez y va de izquierda a derecha (omitiendo algunos valores), puede usar en shiftlugar de índices variables.

# List elements must not contain whitespace or ?*\[
list='1 2 3 5 9 8 6 90 84 9 3 2 15 75 55'
set $list
while [ $# -ge 1 ]; do
  echo "$1"
  shift && shift && shift
done

El enfoque más rápido depende del shell y del número de elementos.

Otra posibilidad es utilizar el procesamiento de cadenas. Tiene la ventaja de no usar los parámetros posicionales, por lo que puede usarlos para otra cosa. Será más lento para grandes cantidades de datos, pero es poco probable que haga una diferencia notable para pequeñas cantidades de datos.

# List elements must be separated by a single space (not arbitrary whitespace)
list='1 2 3 5 9 8 6 90 84 9 3 2 15 75 55'
while [ -n "$list" ]; do
  echo "${list% *}"
  case "$list" in *\ *\ *\ *) :;; *) break;; esac
  list="${list#* * * }"
done
Gilles 'SO- deja de ser malvado'
fuente
" Por otro lado, es probable que un bucle de shell que itera sobre una cadena grande o una gran cantidad de cadena sea más lento que una invocación de una herramienta de propósito especial ", pero ¿y si esa herramienta tiene bucles como awk? @ikkachu dijo que los bucles awk son más rápidos, pero ¿diría que con <1000 campos para iterar, el beneficio de los bucles más rápidos no superaría el costo de llamar a awk ya que es un comando externo (suponiendo que pueda hacer la misma tarea en shell bucles con el uso de solo comandos integrados)?
Levi Uzodike
@LeviUzodike Vuelva a leer el primer párrafo de mi respuesta.
Gilles 'SO- deja de ser malvado'
También puede sustituir shift && shift && shiftcon shift 3en su tercer ejemplo - a menos que la cáscara está utilizando no lo soporta.
Joe
2
@ Joe En realidad, no. shift 3fallaría si quedaran muy pocos argumentos restantes. Necesitarías algo comoif [ $# -gt 3 ]; then shift 3; else set --; fi
Gilles 'SO- deja de ser malvado'
3

awkes una gran opción, si puede hacer todo su procesamiento dentro del script Awk. De lo contrario, terminas canalizando la salida Awk a otras utilidades, destruyendo la ganancia de rendimiento de awk.

bashLa iteración sobre una matriz también es excelente, si puede ajustar su lista completa dentro de la matriz (lo que para los shells modernos es probablemente una garantía) y no le importa la gimnasia de sintaxis de matriz.

Sin embargo, un enfoque de tubería:

xargs -n3 <<< "$list" | while read -ra a; do echo $a; done | grep 9

Dónde:

  • xargs agrupa la lista separada por espacios en blanco en lotes de tres, cada nueva línea separada
  • while read consume esa lista y genera la primera columna de cada grupo
  • grep filtra la primera columna (correspondiente a cada tercera posición en la lista original)

Mejora la comprensibilidad, en mi opinión. La gente ya sabe lo que hacen estas herramientas, por lo que es fácil leer de izquierda a derecha y razonar sobre lo que va a suceder. Este enfoque también documenta claramente la longitud del paso ( -n3) y el patrón de filtro ( 9), por lo que es fácil de variabilizar:

count=3
find=9
xargs -n "$count" <<< "$list" | while read -ra a; do echo $a; done | grep "$find"

Cuando hagamos preguntas de "eficiencia", asegúrese de pensar en la "eficiencia total de por vida". Ese cálculo incluye el esfuerzo de los mantenedores para mantener el código funcionando, y las bolsas de carne somos las máquinas menos eficientes en toda la operación.

obispo
fuente
2

Quizás esto?

cut -d' ' -f1,4,7,10,13 <<<$list
1 5 6 9 15
doneal24
fuente
Lo siento, no estaba claro antes, pero quería poder obtener los números en esas posiciones sin saber la longitud de la lista. Pero gracias, olvidé que cortar podría hacer eso.
Levi Uzodike
1

No use comandos de shell si quiere ser eficiente. Limítese a tuberías, redirecciones, sustituciones, etc. y programas. Por eso xargsy parallelutilidades existe - a causa fiesta, mientras que los bucles son ineficientes y muy lento. Use bash loops solo como la última resolución.

list="1 ant bat 5 cat dingo 6 emu fish 9 gecko hare 15 i j"
if 
    <<<"$list" tr -d -s '[0-9 ]' | 
    tr -s ' ' | tr ' ' '\n' | 
    grep -q -x '9'
then
    found=true
else 
    found=false
fi
echo ${found} 

Pero probablemente deberías ser algo más rápido con el bien awk.

KamilCuk
fuente
Lo siento, no estaba claro antes, pero estaba buscando una solución que pudiera extraer los valores basados ​​solo en su posición en la lista. Acabo de hacer la lista original así porque quería que fuera obvio los valores que quería.
Levi Uzodike
1

En mi opinión, la solución más clara (y probablemente la más eficiente también) es usar las variables awk de RS y ORS:

awk -v RS=' ' -v ORS=' ' 'NR % 3 == 1' <<< "$list"
user000001
fuente
1
  1. Usando script de shell GNU sed y POSIX :

    echo $(printf '%s\n' $list | sed -n '1~3p')
  2. O con bashla sustitución de parámetros :

    echo $(sed -n '1~3p' <<< ${list// /$'\n'})
  3. No GNU ( es decir, POSIX ) sedy bash:

    sed 's/\([^ ]* \)[^ ]* *[^ ]* */\1/g' <<< "$list"

    O de manera más portátil, utilizando POSIX sed y script de shell:

    echo "$list" | sed 's/\([^ ]* \)[^ ]* *[^ ]* */\1/g'

Salida de cualquiera de estos:

1 5 6 9 15
agc
fuente