Perché sta usando && 75 volte più velocemente di se ... fi e come rendere il codice più chiaro


38

Ho il seguente codice di lavoro:

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"

Questo codice viene eseguito rapidamente è 0,194 secondi. Tuttavia ho trovato && is_prime= falseun po 'difficile da leggere e potrebbe sembrare (all'occhio non allenato) come se fosse stato testato piuttosto che essere impostato, che è quello che fa. Quindi ho provato a cambiarlo &&in an if...thene questo funziona - ma è 75 volte più lento a 14,48 secondi. È più evidente sui numeri più alti.

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"

C'è qualcuno che doveva avere la chiarezza del blocco senza la lentezza?

Aggiornamento (1/4/2015 10:40 EST)

Ottimo riscontro! Ora sto usando il seguente. Qualche altro commento ?

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"

1
Su un sidenote, l'esecuzione del tuo script lo stampa Largest Prime= 100sul mio computer.
Giulio Muscarello,

3
Inoltre, se sei interessato all'efficienza, un modo banale per migliorare questo sarebbe quello di iterare solo number_under_test/2invece di fino a number_under_test-1: nessun fattore di un numero n è maggiore di n / 2, quindi troverai ancora tutto fattori per i numeri non primi facendo questo. (Anche se tu fossi interessato solo a testare il primeness, sarebbe sufficiente iterare fino a sqrt (n), ma Bash non ha comunque una funzione integrata per calcolare le radici quadrate.)
Malte Skoruppa

Matte, buon punto (+1). L'unica modifica è stata che non ha funzionato per il numero 4, quindi ho dovuto farlo (number_under_test/2)+1per consentirlo
Michael Durrant,

1
Nella vostra versione aggiornata, le parentesi {}non sono realmente necessari dopo la thenclausola perché il thenserve già come operatore di raggruppamento (insieme a elif, elseo fi). In effetti, in alcune shell, è possibile scrivere, ad esempio, for i in 1 2 3; { echo $i; }senza doo done.
Jonathan Leffler,

1
+1 Jonathan, ho apportato le modifiche e aggiornato l'aggiornamento
Michael Durrant,

Risposte:


66

Questo perché stai generando una sotto-shell ogni volta:

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

Basta rimuovere le parentesi

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

Se vuoi raggruppare i comandi, c'è la sintassi per farlo nella shell corrente :

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

(è richiesto il punto e virgola finale, consultare il manuale )

Nota che [ is_prime ]non è lo stesso di [ $is_prime == true ]: potresti scriverlo semplicemente $is_prime(senza parentesi) che invocherebbe il comando incorporato trueo bash false.
[ is_prime ]è un test con un argomento, la stringa "is_prime" - quando [viene dato un singolo argomento, il risultato è successo se l'argomento è non vuoto e quella stringa letterale è sempre non vuota, quindi sempre "vera".

Per leggibilità, cambierei la linea molto lunga

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

a

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

Non sottovalutare gli spazi bianchi per migliorare la chiarezza.


1
c'è un largest_prime=$number_under_testerrore di
battitura

1
Vale anche la pena notare che in bash, zsh, et al, [sta invocando un programma chiamato letteralmente [, mentre [[è implementato nella shell - quindi sarà più veloce. Prova time for ((i = 0; $i < 1000; i++)); do [ 1 ]; donee confronta con [[. Vedi questa domanda SO per ulteriori informazioni.
Kirb,

2
bash implementa [, è un builtin. Da un prompt della shell, digitare type -a [ehelp [
glenn jackman,

@glennjackman Wow; non ne ero a conoscenza. Ho pensato che fosse ancora il caso perché which [ritorna ancora /usr/bin/[. Mi sono anche reso conto che insinuavo che zsh era lo stesso; per me questo mi dice che è incorporato. Ma poi ... perché [[più veloce?
Kirb,

2
@glennjackman command -vè un'altra buona whichalternativa; vedi anche qui .
Abbafei,

9

Penso che tu stia lavorando troppo duramente per quella tua funzione. Ritenere:

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'aritmetica della shell è abbastanza in grado di valutare da sola le condizioni dei numeri interi. Raramente ha bisogno di troppi test e / o incarichi esterni. Questo whileloop duplica abbastanza bene i tuoi loop nidificati:

Non stampa così tanto, ovviamente, non ho scritto molto, ma, per esempio, impostando il soffitto su 16 anziché su 101 come è scritto sopra e ...

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

Sta sicuramente facendo il lavoro. E richiede pochissimo altro per approssimare il tuo output:

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

Lo sto facendo piuttosto che il echo...

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

Questo funziona in busybox. È molto portatile, veloce e facile da usare.

Il problema della subshell si verificherà nella maggior parte delle shell, ma è di gran lunga più acuto in una bashshell. Ho alternato il fare

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

... e il modo in cui l'ho scritto sopra in diverse shell per un limite di 101 e l' dashho fatto senza la subshell in 0,017 secondi e con la subshell in 1,8 secondi. busybox.149 e 2, zsh .149 e 4, bash.35 e 6, e ksh93in .149 e .160. ksh93non effettua il fork per i subshells come devono fare gli altri shell. Quindi forse il problema non è tanto la subshell quanto la shell .


Qual è il vantaggio di [ "$((...))" -eq "$((...))" ]over (( (...) == (...) ))? Quest'ultimo è meno portatile?
Ruakh

@ruakh: portabilità, velocità, affidabilità. [ "$((...))" -eq "$((...)) ]funziona in shell che non impiegano 15 secondi per eseguire il programma, e l'altro no. Se il vantaggio dell'uno sull'altro è discutibile, ciò può solo dare un vantaggio al primo, il che significa che non c'è mai un buon motivo per usarlo (( (...) == (...) )).
Mikeserv,

Siamo spiacenti, ma la tua risposta sembra presumere che io abbia già una conoscenza dettagliata del supporto shell (( ... )). Sono lusingato, ma non senza avere quella conoscenza dettagliata. (Ricorda, sono io quello che ha appena chiesto se (( ... ))è meno portatile.) Quindi non riesco davvero a dare un senso alla tua risposta. : - / Potresti essere un po 'più esplicito?
Ruakh

@ruakh - Mi dispiace ... non ho visto che stavi chiedendo se fosse più portatile, quanto fosse vantaggioso - motivo per cui ho risposto sulla portabilità. Comunque, "$((...))"è specificato POSIX e l'altro è un'estensione shell. Le shell POSIX sono abbastanza capaci. Anche dashe poshgestirà correttamente i test dei rami come "$((if_true ? (var=10) : (var=5) ))"e assegnerà sempre $varcorrettamente. busyboxsi rompe lì - elimina sempre entrambe le parti indipendentemente dal $if_truevalore.
Mikeserv,

@ruakh - oh amico. Devo essere un po 'fuori oggi ... dice proprio lì ... quest'ultimo è meno portatile ? Non l'ho mai visto prima, immagino ...?
Mikeserv,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.