La tuyauterie, le décalage ou l'expansion des paramètres est-il plus efficace?

26

J'essaie de trouver le moyen le plus efficace d'itérer à travers certaines valeurs qui sont un nombre cohérent de valeurs éloignées les unes des autres dans une liste de mots séparés par des espaces (je ne veux pas utiliser un tableau). Par exemple,

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

Je veux donc pouvoir simplement parcourir la liste et accéder uniquement aux 1,5,6,9 et 15.

EDIT: J'aurais dû indiquer clairement que les valeurs que j'essaie d'obtenir de la liste ne doivent pas être de format différent du reste de la liste. Ce qui les rend spéciaux, c'est uniquement leur position dans la liste (dans ce cas, position 1,4,7 ...). Donc, la liste pourrait être,1 2 3 5 9 8 6 90 84 9 3 2 15 75 55mais je voudrais toujours les mêmes chiffres. Et aussi, je veux pouvoir le faire en supposant que je ne connais pas la longueur de la liste.

Les méthodes auxquelles j'ai pensé jusqu'à présent sont les suivantes:

Méthode 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éthode 2

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

Méthode 3 Je suis presque sûr que la tuyauterie en fait la pire option, mais j'essayais de trouver une méthode qui n'utilise pas set, par curiosité.

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

Alors, qu'est-ce qui serait le plus efficace ou manque-t-il une méthode plus simple?

Levi Uzodike
la source
10
Je n'utiliserais pas un script shell en premier lieu si l'efficacité est une préoccupation importante. Quelle est la taille de votre liste qui fait une différence?
Barmar
2
Sans faire de statistiques sur les instances réelles de votre problème, vous ne saurez rien. Cela inclut la comparaison avec la "programmation en awk", etc. Si les statistiques sont trop chères, la recherche d'efficacité ne vaut probablement pas la peine.
David Tonhofer
2
Levi, quelle est exactement la manière "efficace" dans votre définition? Vous souhaitez trouver un moyen plus rapide d'itérer?
Sergiy Kolodyazhnyy

Réponses:

18

Assez simple avec awk. Cela vous donnera la valeur de chaque quatrième champ pour une entrée de n'importe quelle longueur:

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

Cela fonctionne en tirant parti des awkvariables intégrées telles que NF(le nombre de champs dans l'enregistrement) et en effectuant une forboucle simple pour parcourir les champs pour vous donner celles que vous voulez sans avoir besoin de savoir à l'avance combien il y en aura.

Ou, si vous voulez en effet simplement ces champs spécifiques comme spécifié dans votre exemple:

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

En ce qui concerne la question de l'efficacité, la voie la plus simple serait de tester cette ou chacune de vos autres méthodes et de l'utiliser timepour montrer combien de temps cela prend; vous pouvez également utiliser des outils tels que stracepour voir le flux des appels système. L'utilisation de timeressemble à:

$ time ./script.sh

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

Vous pouvez comparer cette sortie entre différentes méthodes pour voir laquelle est la plus efficace en termes de temps; d'autres outils peuvent être utilisés pour d'autres mesures d'efficacité.

DopeGhoti
la source
1
Bon point, @MichaelHomer; J'ai ajouté un côté abordant la question de "comment puis-je déterminer quelle méthode est la plus efficace ".
DopeGhoti
2
@LeviUzodike En ce qui concerne echovs <<<, "identique" est un mot trop fort. On pourrait dire que stuff <<< "$list"c'est presque identique à printf "%s\n" "$list" | stuff. Concernant echovs printf, je vous
redirige
5
@DopeGhoti En fait, c'est le cas. <<<ajoute une nouvelle ligne à la fin. Ceci est similaire à la façon dont $()supprime une nouvelle ligne de la fin. En effet, les lignes se terminent par des retours à la ligne. <<<alimente une expression sous forme de ligne, elle doit donc se terminer par une nouvelle ligne. "$()"prend des lignes et les fournit comme argument, il est donc logique de convertir en supprimant la nouvelle ligne de fin.
JoL
3
@LeviUzodike awk est un outil très sous-estimé. Cela rendra toutes sortes de problèmes apparemment complexes faciles à résoudre. Surtout lorsque vous essayez d'écrire une expression rationnelle complexe pour quelque chose comme sed, vous pouvez souvent gagner des heures en l'écrivant de manière procédurale en awk. L'apprentissage paiera des dividendes importants.
Joe
1
@LeviUzodike: Oui, awkc'est un binaire autonome qui doit démarrer. Contrairement à Perl ou à Python en particulier, l'interpréteur awk démarre rapidement (toujours tous les frais généraux de l'éditeur de liens dynamiques pour faire pas mal d'appels système, mais awk n'utilise que libc / libm et libdl. Par exemple, utilisez stracepour vérifier les appels système du démarrage awk) . De nombreux shells (comme bash) sont assez lents, donc lancer un processus awk peut être plus rapide que de boucler sur des jetons dans une liste avec des shell intégrés même pour des tailles de liste de petite taille. Et parfois, vous pouvez écrire un #!/usr/bin/awkscript au lieu d'un #!/bin/shscript.
Peter Cordes
35
  • Première règle d'optimisation logicielle: ne le faites pas .

    Tant que vous ne savez pas que la vitesse du programme est un problème, il n'est pas nécessaire de penser à quelle vitesse il est. Si votre liste est de cette longueur ou seulement de 100 à 1000 articles, vous ne remarquerez probablement même pas combien de temps cela prend. Il est possible que vous passiez plus de temps à penser à l'optimisation qu'à la différence.

  • Deuxième règle: mesurer .

    C'est le moyen sûr de le savoir et celui qui donne des réponses pour votre système. Surtout avec des coquillages, il y en a tellement, et ils ne sont pas tous identiques. Une réponse pour un shell peut ne pas s'appliquer à la vôtre.

    Dans les programmes plus importants, le profilage va ici aussi. La partie la plus lente n'est peut-être pas celle que vous pensez.

  • Troisièmement, la première règle d'optimisation du script shell: n'utilisez pas le shell .

    Ouais vraiment. De nombreux shells ne sont pas faits pour être rapides (car le lancement de programmes externes ne doit pas l'être), et ils peuvent même analyser à nouveau les lignes du code source à chaque fois.

    Utilisez plutôt quelque chose comme awk ou Perl. Dans un micro-benchmark trivial que j'ai fait, awkétait des dizaines de fois plus rapide que n'importe quel shell commun pour exécuter une boucle simple (sans E / S).

    Cependant, si vous utilisez le shell, utilisez les fonctions internes du shell au lieu des commandes externes. Ici, vous utilisez exprce qui n'est intégré à aucun shell que j'ai trouvé sur mon système, mais qui peut être remplacé par une expansion arithmétique standard. Par exemple, i=$((i+1))au lieu d' i=$(expr $i + 1)incrémenter i. Votre utilisation de cutdans le dernier exemple peut également être remplaçable par des extensions de paramètres standard.

    Voir aussi: Pourquoi l'utilisation d'une boucle shell pour traiter du texte est-elle considérée comme une mauvaise pratique?

Les étapes 1 et 2 doivent s'appliquer à votre question.

ilkkachu
la source
12
# 0, citez vos extensions :-)
Kusalananda
8
Ce n'est pas que les awkboucles soient nécessairement meilleures ou pires que les boucles shell. C'est que le shell est vraiment bon pour exécuter des commandes et pour diriger les entrées et sorties vers et depuis les processus, et franchement plutôt maladroit pour tout le reste; alors que les outils comme awksont fantastiques pour traiter les données de texte, parce que c'est pour cela que les coquilles et les outils awksont conçus (respectivement) en premier lieu.
DopeGhoti
2
@DopeGhoti, les obus semblent cependant objectivement plus lents. Certaines boucles très simples semblent être> 25 fois plus lentes dashqu'avec gawk, et dashc'était le shell le plus rapide que j'ai testé ...
ilkkachu
1
@Joe, c'est :) dashet busyboxne supporte pas (( .. ))- je pense que c'est une extension non standard. ++est également mentionné explicitement comme non requis, pour autant que je sache, i=$((i+1))ou : $(( i += 1))sont les plus sûrs.
ilkkachu
1
Re "penser plus de temps" : cela néglige un facteur important. À quelle fréquence s'exécute-t-il et pour combien d'utilisateurs? Si un programme gaspille 1 seconde, ce qui pourrait être corrigé par le programmeur en y réfléchissant pendant 30 minutes, cela pourrait être une perte de temps s'il n'y a qu'un seul utilisateur qui va l'exécuter une fois. D'un autre côté, s'il y a un million d'utilisateurs, c'est un million de secondes, soit 11 jours de temps utilisateur. Si le code a gaspillé une minute d'un million d'utilisateurs, cela représente environ 2 ans de temps utilisateur.
agc
13

Je vais seulement donner quelques conseils généraux dans cette réponse, et non des repères. Les repères sont le seul moyen de répondre de manière fiable aux questions sur les performances. Mais comme vous ne dites pas la quantité de données que vous manipulez et la fréquence à laquelle vous effectuez cette opération, il n'y a aucun moyen de faire un benchmark utile. Ce qui est plus efficace pour 10 éléments et ce qui est plus efficace pour 1000000 éléments n'est souvent pas le même.

En règle générale, invoquer des commandes externes coûte plus cher que de faire quelque chose avec des constructions de shell pur, tant que le code de shell pur n'implique pas de boucle. D'un autre côté, une boucle shell qui itère sur une grande chaîne ou une grande quantité de chaîne est susceptible d'être plus lente qu'une invocation d'un outil spécial. Par exemple, votre appel de boucle cutpourrait être sensiblement lent dans la pratique, mais si vous trouvez un moyen de faire le tout avec une seule cutinvocation, cela sera probablement plus rapide que de faire la même chose avec la manipulation de chaînes dans le shell.

Notez que le point de coupure peut varier considérablement d'un système à l'autre. Cela peut dépendre du noyau, de la configuration de l'ordonnanceur du noyau, du système de fichiers contenant les exécutables externes, de la quantité de pression CPU vs mémoire actuellement, et de nombreux autres facteurs.

N'appelez pas exprpour effectuer de l'arithmétique si vous êtes préoccupé par la performance. En fait, n'appelez pas du tout exprpour effectuer de l'arithmétique. Les shells ont une arithmétique intégrée, qui est plus claire et plus rapide que l'invocation expr.

Vous semblez utiliser bash, puisque vous utilisez des constructions bash qui n'existent pas dans sh. Alors pourquoi diable n'utilisez-vous pas un tableau? Un tableau est la solution la plus naturelle, et il est probable qu'elle soit aussi la plus rapide. Notez que les indices de tableau commencent à 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

Votre script pourrait bien être plus rapide si vous utilisez sh, si votre système a dash ou ksh shplutôt que bash. Si vous utilisez sh, vous n'obtenez pas de tableaux nommés, mais vous obtenez tout de même un tableau de paramètres positionnels, que vous pouvez définir avec set. Pour accéder à un élément à une position qui n'est pas connue avant l'exécution, vous devez utiliser eval(prenez soin de citer les choses correctement!).

# 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 vous ne souhaitez accéder au tableau qu'une seule fois et que vous allez de gauche à droite (en sautant certaines valeurs), vous pouvez utiliser à la shiftplace des indices de variable.

# 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

L'approche la plus rapide dépend du shell et du nombre d'éléments.

Une autre possibilité consiste à utiliser le traitement de chaîne. Il a l'avantage de ne pas utiliser les paramètres de position, vous pouvez donc les utiliser pour autre chose. Ce sera plus lent pour de grandes quantités de données, mais il est peu probable que cela fasse une différence notable pour de petites quantités de données.

# 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- arrête d'être méchant'
la source
" D'un autre côté, une boucle shell qui itère sur une grande chaîne ou une grande quantité de chaîne est susceptible d'être plus lente qu'une invocation d'un outil spécial ", mais que se passe-t-il si cet outil contient des boucles comme awk? @ikkachu a déclaré que les boucles awk sont plus rapides, mais diriez-vous qu'avec <1000 champs à parcourir, l'avantage des boucles plus rapides ne l'emporterait pas sur le coût de l'appel awk car c'est une commande externe (en supposant que je puisse faire la même tâche dans le shell boucles avec l’utilisation de commandes intégrées uniquement)?
Levi Uzodike
@LeviUzodike Veuillez relire le premier paragraphe de ma réponse.
Gilles 'SO- arrête d'être méchant'
Vous pouvez également remplacer shift && shift && shiftpar shift 3dans votre troisième exemple - sauf si le shell que vous utilisez ne le prend pas en charge.
Joe
2
@Joe En fait, non. shift 3échouerait s'il restait trop peu d'arguments. Vous auriez besoin de quelque chose commeif [ $# -gt 3 ]; then shift 3; else set --; fi
Gilles 'SO- arrête d'être méchant'
3

awkest un excellent choix, si vous pouvez faire tout votre traitement à l'intérieur du script Awk. Sinon, vous finissez par diriger la sortie Awk vers d'autres utilitaires, détruisant le gain de performances de awk.

bashl'itération sur un tableau est également excellente, si vous pouvez insérer toute votre liste à l'intérieur du tableau (ce qui pour les shells modernes est probablement une garantie) et cela ne vous dérange pas la gymnastique de la syntaxe du tableau.

Cependant, une approche de pipeline:

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

Où:

  • xargs regroupe la liste séparée par des espaces en lots de trois, chaque nouvelle ligne étant séparée
  • while read consomme cette liste et affiche la première colonne de chaque groupe
  • grep filtre la première colonne (correspondant à chaque troisième position dans la liste d'origine)

Améliore la compréhensibilité, à mon avis. Les gens savent déjà ce que font ces outils, il est donc facile de lire de gauche à droite et de raisonner sur ce qui va se passer. Cette approche documente également clairement la longueur de foulée ( -n3) et le modèle de filtre ( 9), il est donc facile de la varier:

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

Lorsque nous posons des questions sur «l'efficacité», pensez à «l'efficacité totale de la durée de vie». Ce calcul inclut l'effort des mainteneurs pour maintenir le code en fonctionnement, et nous les sacs de viande sont les machines les moins efficaces de toute l'opération.

évêque
la source
2

Peut-être cela?

cut -d' ' -f1,4,7,10,13 <<<$list
1 5 6 9 15
doneal24
la source
Désolé, je n'étais pas clair avant, mais je voulais pouvoir obtenir les chiffres à ces postes sans connaître la longueur de la liste. Mais merci, j'ai oublié que cut pouvait faire ça.
Levi Uzodike
1

N'utilisez pas de commandes shell si vous voulez être efficace. Limitez-vous aux canaux, aux redirections, aux substitutions, etc. et aux programmes. C'est pourquoi xargset les parallelutilitaires existent - parce que les boucles bash while sont inefficaces et très lentes. Utilisez les boucles bash uniquement comme dernière résolution.

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} 

Mais vous devriez probablement être un peu plus rapide avec un bon awk.

KamilCuk
la source
Désolé, je n'étais pas clair avant, mais je cherchais une solution qui pourrait extraire les valeurs uniquement en fonction de leur position dans la liste. Je viens de faire la liste originale comme ça parce que je voulais que ce soit évident les valeurs que je voulais.
Levi Uzodike
1

À mon avis, la solution la plus claire (et probablement la plus performante aussi) est d'utiliser les variables awk RS et ORS:

awk -v RS=' ' -v ORS=' ' 'NR % 3 == 1' <<< "$list"
user000001
la source
1
  1. Utilisation du script shell GNU sed et POSIX :

    echo $(printf '%s\n' $list | sed -n '1~3p')
  2. Ou avec bashla substitution de paramètres de :

    echo $(sed -n '1~3p' <<< ${list// /$'\n'})
  3. Non- GNU ( c'est-à-dire POSIX ) sedet bash:

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

    Ou plus facilement, en utilisant à la fois POSIX sed et un script shell:

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

Sortie de l'un de ces éléments:

1 5 6 9 15
agc
la source