web-dev-qa-db-fra.com

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

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û préciser que les valeurs que j'essaye d'obtenir de la liste n'ont pas à avoir un 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 ...). Ainsi, la liste pourrait être1 2 3 5 9 8 6 90 84 9 3 2 15 75 55 mais je voudrais toujours les mêmes numéros. 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 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, quelle serait la plus efficace, ou manque-t-il une méthode plus simple?

26
Levi Uzodike

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 variables awk intégrées telles que NF (le nombre de champs dans l'enregistrement), et en effectuant une simple boucle de for pour itérer le long des champs pour donner vous ceux 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 d'utiliser time pour montrer combien de temps cela prend; vous pouvez également utiliser des outils comme strace pour voir comment les appels système se déroulent. L'utilisation de time ressemble à:

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

18
DopeGhoti
  • Première règle d'optimisation logicielle: Ne 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 ordinaire pour exécuter une boucle simple (sans E/S).

    Cependant, si vous utilisez le Shell, utilisez les fonctions intégrées du Shell au lieu des commandes externes. Ici, vous utilisez expr 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 de i=$(expr $i + 1) pour incrémenter i. Votre utilisation de cut dans le dernier exemple peut également être remplacée 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.

35
ilkkachu

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 puisque vous ne dites pas combien données que vous manipulez et à quelle fréquence 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 Shell pures, tant que le code 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 boucle en invoquant cut pourrait être sensiblement lente en pratique, mais si vous trouvez un moyen de faire le tout avec une seule invocation de cut qui sera probablement plus rapide que de faire la même chose 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 façon dont le planificateur du noyau est configuré, 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 expr pour effectuer l'arithmétique si vous êtes préoccupé par les performances. En fait, n'appelez pas expr pour effectuer l'arithmétique. Les shells ont une arithmétique intégrée, qui est plus claire et plus rapide que d'appeler 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 comme sh plutôt que bash. Si vous utilisez sh, vous n'obtenez pas de tableaux nommés, mais vous obtenez tout de même le 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 shift au lieu 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

awk est un excellent choix, if 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.

bash l'itération sur un tableau est également très bien, 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 syntaxe du tableau gymnastique.

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 à une position sur trois 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 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 vie". Ce calcul inclut l'effort des mainteneurs pour faire fonctionner le code, et nous les sacs de viande sont les machines les moins efficaces de toute l'opération.

3
bishop

Peut-être cela?

cut -d' ' -f1,4,7,10,13 <<<$list
1 5 6 9 15
2
doneal24

N'utilisez pas les commandes Shell si vous voulez être efficace. Limitez-vous aux canaux, aux redirections, aux substitutions, etc. et aux programmes. C'est pourquoi les utilitaires xargs et parallel 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.

1
KamilCuk
  1. Utilisation de GNU sed et POSIX Script shell:

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

    echo $(sed -n '1~3p' <<< ${list// /$'\n'})
    
  3. Non - GNU (ie POSIX ) sed et bash:

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

    Ou plus facilement, en utilisant à la fois POSIX sed et le script Shell:

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

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

1 5 6 9 15
1
agc

À 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"
1
user000001