Warum ist die Verwendung von && 75-mal schneller als wenn ... fi und wie lässt sich Code klarer darstellen?

38

Ich habe den folgenden Arbeitscode:

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"

Dieser Code läuft schnell ist 0,194 Sekunden. Ich fand das jedoch && is_prime= falseein bisschen schwer zu lesen und es könnte (für das ungeübte Auge) so aussehen, als würde es getestet, anstatt gesetzt zu werden, was es tut. Also habe ich versucht das &&in ein zu ändern if...thenund das funktioniert - ist aber 75 mal langsamer bei 14,48 Sekunden. Am deutlichsten macht sich dies bei den höheren Zahlen bemerkbar.

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"

Gibt es eine, die die Klarheit des Blocks ohne Langsamkeit haben sollte?

Aktualisierung (04.01.2015, 10:40 Uhr EST)

Tolles Feedback! Ich benutze jetzt die folgenden. Irgendwelche anderen Rückmeldungen ?

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
quelle
1
Wenn Sie Ihr Skript nebenbei ausführen, wird dies Largest Prime= 100auf meinem Computer gedruckt .
Giulio Muscarello
3
Wenn Sie an Effizienz interessiert sind, besteht eine einfache Möglichkeit, dies zu verbessern, darin, nur bis zu number_under_test/2statt bis zu iterieren number_under_test-1: Kein Faktor einer Zahl n ist größer als n / 2, sodass Sie immer noch alle finden Faktoren für Nicht-Primzahlen auf diese Weise. (Auch wenn Sie nur auf Primheit testen möchten, ist es ausreichend, bis zu sqrt (n) zu iterieren, aber Bash verfügt ohnehin nicht über eine integrierte Funktion zum Berechnen von Quadratwurzeln.)
Malte Skoruppa
Matt, guter Punkt (+1). Die einzige Änderung war, dass es für die Nummer 4 nicht funktionierte, also musste ich es schaffen, um das (number_under_test/2)+1zu ermöglichen
Michael Durrant
1
In Ihrer aktualisierte Version, die geschweiften Klammern {}sind nicht wirklich nach der benötigten thenKlausel , da die thenbereits als Gruppierungs Operator dienen (zusammen mit elif, elseoder fi). Tatsächlich könnte man in einigen Muscheln beispielsweise for i in 1 2 3; { echo $i; }ohne dooder schreiben done.
Jonathan Leffler
1
+1 Jonathan, ich habe diese Änderungen vorgenommen und das Update aktualisiert
Michael Durrant

Antworten:

66

Das liegt daran, dass Sie jedes Mal eine Unterschale erzeugen:

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

Entfernen Sie einfach die Klammern

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

Wenn Sie Befehle gruppieren möchten, gibt es in der aktuellen Shell die folgende Syntax :

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

(das abschließende Semikolon ist erforderlich, siehe Handbuch )

Beachten Sie, dass dies [ is_prime ]nicht dasselbe ist wie [ $is_prime == true ]: Sie können dies einfach $is_prime(ohne eckige Klammern) schreiben, wodurch die integrierte Bash trueoder der falseBefehl aufgerufen werden .
[ is_prime ]ist ein Test mit einem Argument, der Zeichenfolge "is_prime" - Wenn [ein einzelnes Argument angegeben wird, ist das Ergebnis erfolgreich, wenn das Argument nicht leer ist und diese Literalzeichenfolge immer nicht leer ist, daher immer "true".

Aus Gründen der Lesbarkeit würde ich die sehr lange Zeile ändern

[ $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

zu

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

Unterschätzen Sie Leerzeichen nicht, um die Übersichtlichkeit zu verbessern.

Glenn Jackman
quelle
1
es gibt einen Tippfehler - largest_prime=$number_under_testsollte in der damaligen Branche sein (der gleiche Fehler ist im Original)
JJoao
1
Es ist auch erwähnenswert, dass zsh et al in bash [ein Programm aufrufen, das buchstäblich aufgerufen wird [, während [[es in der Shell implementiert ist - daher ist es schneller. Versuchen Sie time for ((i = 0; $i < 1000; i++)); do [ 1 ]; doneund vergleichen Sie mit [[. Siehe diese SO-Frage für weitere Informationen.
kirb
2
Bash-Geräte [, es ist eine eingebaute. Von einer Shell - Eingabeaufforderung ein type -a [undhelp [
glenn jackman
@glennjackman Wow; war sich dessen nicht bewusst. Ich nahm an, dass es immer noch so ist, weil es which [immer noch zurückkommt /usr/bin/[. Ich habe auch gerade gemerkt, dass ich angedeutet habe, dass zsh dasselbe ist. für mich bedeutet das, dass es ein eingebautes ist. Aber dann ... warum ist [[schneller?
kirb
2
@glennjackman command -vist eine weitere gute whichAlternative; siehe auch hier .
Abbafei,
9

Ich denke, Sie arbeiten viel zu hart an Ihrer Funktion. Erwägen:

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

Die Shell-Arithmetik ist in der Lage, ganzzahlige Bedingungen selbstständig auszuwerten. Es werden selten zu viele Tests und / oder externe Aufgaben benötigt. Diese eine whileSchleife dupliziert Ihre verschachtelten Schleifen ziemlich gut:

Es wird natürlich nicht so viel gedruckt, ich habe nicht so viel geschrieben, aber zum Beispiel die Obergrenze auf 16 anstatt auf 101 gesetzt, wie oben geschrieben und ...

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

Es macht definitiv die Arbeit. Und es erfordert sehr wenig anderes, um Ihre Ausgabe zu approximieren:

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

Tu das einfach lieber als das echound ...

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

Das funktioniert in busybox. Es ist sehr portabel, schnell und einfach zu bedienen.

Ihr Subshell-Problem tritt in den meisten Shells auf, ist jedoch in einer Shell mit Abstand am akutesten bash. Ich wechselte zwischen tun

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

... und so habe ich es oben in mehreren Shells für eine Decke von 101 geschrieben und dashes ohne die Subshell in 0,017 Sekunden und mit der Subshell in 1,8 Sekunden gemacht. busybox.149 und 2, zsh .149 und 4, bash.35 und 6 und ksh93in .149 und .160. ksh93Verzweigt sich nicht für Unterschalen, wie die anderen Schalen müssen. Vielleicht liegt das Problem weniger in der Subshell als vielmehr in der Shell .

mikeserv
quelle
Was ist der Vorteil von [ "$((...))" -eq "$((...))" ]over (( (...) == (...) ))? Ist letzteres weniger portabel?
Ruakh
@ruakh - Portabilität, Geschwindigkeit, Zuverlässigkeit. [ "$((...))" -eq "$((...)) ]Funktioniert in Shells, die nicht länger als 15 Sekunden benötigen , um das Programm auszuführen, während dies bei der anderen Version nicht der Fall ist. Wenn der Vorteil des einen gegenüber dem anderen überhaupt fraglich ist, kann dies nur dem ersteren den entscheidenden Vorteil verleihen, was bedeutet, dass es nie einen guten Grund gibt, diesen zu nutzen (( (...) == (...) )).
mikeserv
Entschuldigung, aber Ihre Antwort scheint davon auszugehen, dass ich bereits detaillierte Kenntnisse über die Shell-Unterstützung für habe (( ... )). Ich bin geschmeichelt, aber ich habe nicht das detaillierte Wissen. (Denken Sie daran, ich bin derjenige, der gerade gefragt hat, ob er (( ... ))weniger portabel ist.) Ich kann Ihre Antwort also wirklich nicht verstehen. : - / Könnten Sie etwas expliziter sein?
Ruakh
@ruakh - Es tut mir leid ... Ich habe nicht gesehen, dass Sie gefragt haben, ob es portabler ist, nur wie es vorteilhaft ist - weshalb ich auf Portabilität geantwortet habe. Wie auch immer, das "$((...))"ist POSIX-spezifiziert und das andere ist eine Shell-Erweiterung. POSIX-Shells sind ziemlich leistungsfähig. Auch dashund poshwird richtig Branchentests behandeln "$((if_true ? (var=10) : (var=5) ))"und immer $varrichtig zuordnen . busyboxbricht dort - es bewertet immer beide Seiten unabhängig vom $if_trueWert.
mikeserv
@ruakh - oh man. Ich muss heute ein bisschen weg sein ... es heißt genau dort ... ist letzteres weniger tragbar ? Das habe ich vorher wohl nicht gesehen ...?
mikeserv