Pourquoi utiliser && 75 fois plus rapidement que si… fi et comment rendre le code plus clair

38

J'ai le code de travail suivant:

largest_prime=1
for number_under_test in {1..100}
do
  is_prime=true
  factors=''
  for ((divider = 2; divider < number_under_test-1; divider++));
  do
    remainder=$(($number_under_test % $divider))
    [ $remainder == 0 ] && [ is_prime ] && is_prime=false && factors+=$divider' '
  done
  [ $is_prime == true ] && echo "${number_under_test} is prime!" || echo "${number_under_test} is NOT prime (factors= $factors)"  [ $is_prime == true ] && largest_prime=$number_under_test
done
printf "\nLargest Prime= $largest_prime\n"

Ce code court vite est de 0.194 secondes. Cependant, j’ai trouvé le && is_prime= falselecteur un peu difficile à lire et il pourrait sembler (à l’œil non averti) qu’il était en train d’être testé, plutôt que réglé, ce qui est le cas. Alors j'ai essayé de changer le &&en un if...thenet cela fonctionne - mais est 75 fois plus lent à 14,48 secondes. C'est surtout visible sur les nombres les plus élevés.

largest_prime=1
for number_under_test in {1..100}
do
  is_prime=true
  factors=''
  for ((divider = 2; divider < number_under_test-1; divider++));
  do  
    remainder=$(($number_under_test % $divider))
    if ([ $remainder == 0 ] && [ $is_prime == true ]); then
      is_prime=false
      factors+=$divider' '
    fi  
  done
  [ $is_prime == true ] && echo "${number_under_test} is prime!" || echo "${number_under_test} is NOT prime (factors= $factors)"  [ $is_prime == true ] && largest_prime=$number_under_test
done  
printf "\nLargest Prime= $largest_prime\n"

Y at-il était d'avoir la clarté du bloc sans la lenteur?

Mise à jour (1/4/2015 10:40 am EST)

Excellent retour J'utilise maintenant ce qui suit. Pas d'autres commentaires ?

largest_prime=1
separator=' '
for number_under_test in {1..100}; {
  is_prime=true
  factors=''
  for ((divider = 2; divider < (number_under_test/2)+1; divider++)) {
    remainder=$(($number_under_test % $divider))
    if [ $remainder == 0 ]; then
      is_prime=false
      factors+=$divider' '
    fi
  } 
  if $is_prime; then
    printf "\n${number_under_test} IS prime\n\n"
    largest_prime=$number_under_test
  else
    printf "${number_under_test} is NOT prime, factors are: "
    printf "$factors\n"
  fi
}
printf "\nLargest Prime= $largest_prime\n"
Michael Durrant
la source
1
Sur un sidenote, l’exécution de votre script l’imprime Largest Prime= 100sur mon ordinateur.
Giulio Muscarello
3
Toujours dans le même ordre d'idées, si l'efficacité vous intéresse, un moyen trivial d'améliorer ce système consiste à itérer au number_under_test/2lieu de number_under_test-1: Le facteur n n'est pas supérieur à n / 2, vous pourrez donc toujours trouver tous facteurs pour les nombres non premiers en faisant cela. (De plus, si vous n'étiez intéressé que par le test de primauté, il suffirait d'effectuer une itération jusqu'à sqrt (n), mais Bash n'a de toute façon pas de fonction intégrée permettant de calculer les racines carrées.)
Malte Skoruppa
Matte, bon point (+1). Le seul changement était que cela ne fonctionnait pas pour le numéro 4, donc je devais le faire (number_under_test/2)+1pour permettre cela
Michael Durrant
1
Dans votre version mise à jour, les accolades {}ne sont pas vraiment nécessaires après l' thenarticle , car le thensert déjà comme un opérateur de regroupement (avec elif, elseou fi). En fait, dans certains coquillages, vous pourriez écrire, par exemple, for i in 1 2 3; { echo $i; }sans doou done.
Jonathan Leffler
1
+1 Jonathan, j'ai apporté ces modifications et mis à jour la mise à jour
Michael Durrant le

Réponses:

66

C'est parce que vous créez un sous-shell à chaque fois:

if ([ $remainder == 0 ] && [ $is_prime == true ]); then

Il suffit d'enlever les parenthèses

if [ $remainder == 0 ] && [ $is_prime == true ]; then

Si vous souhaitez grouper des commandes, il existe une syntaxe pour le faire dans le shell actuel :

if { [ $remainder == 0 ] && [ $is_prime == true ]; }; then

(le point-virgule final est obligatoire, voir le manuel )

Notez que ce [ is_prime ]n'est pas la même chose que [ $is_prime == true ]: vous pourriez écrire cela simplement $is_prime(sans crochets) ce qui invoquerait la commande trueou falsecommande intégrée bash .
[ is_prime ]est un test avec un seul argument, la chaîne "is_prime" - lorsqu'un [seul argument est attribué, le résultat est successif si l'argument n'est pas vide et cette chaîne littérale est toujours non vide, d'où toujours "true".

Pour la lisibilité, je changerais la très longue ligne

[ $is_prime == true ] && echo "${number_under_test} is prime!" || echo "${number_under_test} is NOT prime (factors= $factors)"  [ $is_prime == true ] && largest_prime=$number_under_test

à

if [ $is_prime == true ]; then
  echo "${number_under_test} is prime!"
else 
  echo "${number_under_test} is NOT prime (factors= $factors)"
  # removed extraneous [ $is_prime == true ] test that you probably
  # didn't notice off the edge of the screen
  largest_prime=$number_under_test
fi

Ne sous-estimez pas les espaces pour améliorer la clarté.

Glenn Jackman
la source
1
il y a une faute de frappe - largest_prime=$number_under_testdevrait être dans la branche d'alors (la même erreur est dans l'original)
JJoao
1
Il est également intéressant de noter que dans bash, zsh et autres [invoquent un programme appelé littéralement [, alors qu'il [[est implémenté dans le shell - il sera donc plus rapide. Essayez de time for ((i = 0; $i < 1000; i++)); do [ 1 ]; donecomparer à [[. Voir cette question SO pour plus d'informations.
Kirb
2
bash implémente [, c’est une commande intégrée. À l'invite du shell, tapez type -a [ethelp [
glenn jackman le
@ glennjackman Wow; n'était pas au courant de cela. J'ai supposé que c'était toujours le cas parce que which [revient toujours /usr/bin/[. Je viens aussi de me rendre compte que je pensais que zsh était identique; pour moi ça me dit que c'est un construit. Mais alors ... pourquoi [[est plus rapide?
Kirb
2
@ glennjackman command -vest une autre bonne whichalternative; voir aussi ici .
Abbafei
9

Je pense que vous travaillez trop dur sur votre fonction. Considérer:

unset num div lprime; set -- "$((lprime=(num=(div=1))))"
while [     "$((     num += ! ( div *= ( div <= num   ) ) ))" -eq \
            "$((     num *=   ( div += 1 )   <= 101   ))" ]    && {
      set   "$(( ! ( num %      div )         * div   ))"     "$@"
      shift "$(( !    $1 +    ( $1 ==  1 )    *  $#   ))"
}; do [ "$div" -gt "$num" ] && echo "$*"      
done

L'arithmétique Shell est assez capable d'évaluer des conditions entières par elle-même. Il a rarement besoin de trop de tests et / ou de missions externes. Cette whileboucle duplique assez bien vos boucles imbriquées:

Bien sûr, ça n'imprime pas beaucoup, je n'ai pas écrit beaucoup, mais, par exemple, le plafond est fixé à 16 plutôt qu'à 101 comme il est écrit ci-dessus et ...

2
3
4 2
5
6 3 2
7
8 4 2
9 3
10 5 2
11
12 6 4 3 2
13
14 7 2
15 5 3

C'est vraiment faire le travail. Et il en faut très peu pour se rapprocher de votre sortie:

...
do [ "$div" -eq "$num" ] && shift &&
   printf "$num ${1+!}= prime.${1+\t%s\t%s}\n" \
          "factors= $*"                        \
          "lprime=$(( lprime = $# ? lprime : num ))"
done

Je fais juste ça plutôt que le echoet ...

1 = prime.
2 = prime.
3 = prime.
4 != prime.     factors= 2      lprime=3
5 = prime.
6 != prime.     factors= 3 2    lprime=5
7 = prime.
8 != prime.     factors= 4 2    lprime=7
9 != prime.     factors= 3      lprime=7
10 != prime.    factors= 5 2    lprime=7
11 = prime.
12 != prime.    factors= 6 4 3 2        lprime=11
13 = prime.
14 != prime.    factors= 7 2    lprime=13
15 != prime.    factors= 5 3    lprime=13

Cela fonctionne dans busybox. Il est très portable, rapide et facile à utiliser.

Votre problème de sous-coquille va se produire dans la plupart des coquilles, mais il est de loin le plus grave dans une bashcoquille. J'ai alterné entre faire

( [ "$div" -gt "$num" ] ) && ...

... et la façon dont je l'ai écrit ci-dessus dans plusieurs obus pour un plafond de 101. Je l' dashai fait sans sous-shell en 0,017 seconde et avec le sous-shell en 1,8 seconde. busybox.149 et 2, zsh. 149 et 4,bash 0,35 et 6, et ksh93en 0,149 et 0,60. ksh93ne fourche pas pour les sous-coquilles comme les autres coquilles doivent. Alors peut-être que le problème n'est pas tant le sous-shell que le shell .

Mikeserv
la source
Quel est l'avantage de [ "$((...))" -eq "$((...))" ]fini (( (...) == (...) ))? Ce dernier est-il moins portable?
Ruakh
@ruakh - portabilité, rapidité, fiabilité. [ "$((...))" -eq "$((...)) ]fonctionne dans des shells qui ne prennent pas 15 secondes pour exécuter le programme, et l'autre pas. Si l’avantage de l’un sur l’autre est discutable, cela ne peut que donner un avantage à l’ancien, ce qui signifie qu’il n’ya jamais de bonne raison de l’utiliser (( (...) == (...) )).
mikeserv le
Désolé, votre réponse semble supposer que j’ai déjà une connaissance détaillée du support de shell pour (( ... )). Je suis flatté, mais je n'ai pas cette connaissance détaillée. (N'oubliez pas que c'est moi qui viens de demander si le contenu (( ... ))est moins portable.) Je ne peux donc vraiment pas comprendre votre réponse. : - / Pourriez-vous être un peu plus explicite?
Ruakh
@ruakh - Je m'excuse ... Je n'ai pas vu que vous demandiez s'il était plus portable, à quel point c'était avantageux - c'est pourquoi j'ai répondu à propos de la portabilité. Quoi qu'il en soit, le "$((...))"est spécifié par POSIX et l'autre est une extension du shell. Les obus POSIX sont assez capables. Even dashet poshgérera correctement les tests de branche comme "$((if_true ? (var=10) : (var=5) ))"toujours et assignera $varcorrectement. busyboxça casse - ça évalue toujours les deux côtés quelle que soit $if_truela valeur de.
mikeserv le
@ruakh - oh mec. Il faut que je sois un peu en retard aujourd'hui… ça dit tout de suite… ce dernier est-il moins portable ? Je n'avais pas vu ça avant, je suppose ...?
mikeserv le