Por que usar && 75 vezes mais rápido do que se… fi e como tornar o código mais claro

38

Eu tenho o seguinte código de trabalho:

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"

Esse código é executado rapidamente é de 0,194 segundos. No entanto, achei && is_prime= falseum pouco difícil de ler e poderia parecer (para um olho destreinado) como se estivesse sendo testado, em vez de definido como é o que faz. Então, eu tentei mudar o &&para um if...thene isso funciona - mas é 75 vezes mais lento em 14,48 segundos. É mais perceptível nos números mais altos.

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"

Existe algum para ter a clareza do bloco sem a lentidão?

Atualização (1/4/2015 10:40 EST)

Ótimo feedback! Agora estou usando o seguinte. Alguma outra resposta ?

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
fonte
1
Em uma nota lateral, a execução do script o imprime Largest Prime= 100no meu computador.
Giulio Muscarello
3
Também em uma nota lateral, se você estiver interessado em eficiência, uma maneira trivial de melhorar isso seria apenas iterar até em number_under_test/2vez de até number_under_test-1: Nenhum fator de um número n é maior que n / 2; portanto, você ainda encontrará tudo fatores para números não primos, fazendo isso. (Além disso, se você estivesse interessado apenas em testar a primidez, seria suficiente iterar até o sqrt (n), mas o Bash não tem uma função
interna
Fosco, bom ponto (+1). A única mudança foi que não funcionou para o número 4, então eu tive que fazê-lo (number_under_test/2)+1para permitir isso
Michael Durrant
1
Em sua versão atualizada, as chaves {}não são realmente necessários após a thencláusula porque o thenjá serve como um operador de agrupamento (junto com elif, elseou fi). De fato, em algumas conchas, você pode escrever, por exemplo, for i in 1 2 3; { echo $i; }sem doou done.
precisa saber é o seguinte
1
+1 Jonathan, fiz essas alterações e atualizei a atualização #
Michael Durrant

Respostas:

66

Isso ocorre porque você gera um sub-shell toda vez:

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

Basta remover os parênteses

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

Se você deseja agrupar comandos, há uma sintaxe para fazer isso no shell atual :

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

(o ponto e vírgula à direita é necessário, consulte o manual )

Observe que [ is_prime ]não é o mesmo que [ $is_prime == true ]: você poderia escrever isso de maneira simples $is_prime(sem colchetes) que chamaria o comando trueou o built-in do bash false.
[ is_prime ]é um teste com um argumento, a cadeia "is_prime" - quando [é fornecido um argumento único, o resultado é bem-sucedido se o argumento não estiver vazio e essa cadeia literal for sempre não vazia, portanto sempre "verdadeira".

Para facilitar a leitura, eu mudaria a linha muito longa

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

para

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

Não subestime o espaço em branco para melhorar a clareza.

Glenn Jackman
fonte
1
Há um erro ortográfico - largest_prime=$number_under_testdeve ser no ramo, em seguida, (o mesmo erro está no original)
JJoao
1
Também vale a pena notar que no bash, zsh, et al, [está invocando um programa literalmente chamado [, enquanto [[implementado no shell - portanto, será mais rápido. Tente time for ((i = 0; $i < 1000; i++)); do [ 1 ]; donee compare com [[. Veja esta questão para mais informações.
Kirb
2
implementos bash [, é um builtin. A partir de um shell prompt, tipo type -a [ehelp [
glenn jackman
@glennjackman Wow; não estava ciente disso. Presumi que ainda era o caso, porque which [ainda retorna /usr/bin/[. Eu também acabei de perceber que implicava que o zsh era o mesmo; para mim, isso me diz que é um builtin. Mas então ... por que é [[mais rápido?
Kirb
2
@glennjackman command -vé outra boa whichalternativa; veja também aqui .
Abbafei
9

Acho que você está trabalhando demais nessa sua função. Considerar:

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

A aritmética do shell é bastante capaz de avaliar condições inteiras por si só. Raramente precisa de muitos testes e / ou tarefas externas. Esse whileloop duplica os loops aninhados bastante bem:

Não imprime tanto, é claro, eu não escrevi muito, mas, por exemplo, definindo o teto para 16 em vez de 101, como está escrito acima 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

Definitivamente, está fazendo o trabalho. E isso requer muito pouco para aproximar sua saída:

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

Apenas fazendo isso ao invés do echoe ...

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

Isso funciona busybox. É muito portátil, rápido e fácil de usar.

Seu problema de subcamação ocorrerá na maioria das conchas, mas é, de longe , mais agudo em uma bashconcha. Eu alternei entre fazer

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

... e da maneira que escrevi acima em várias conchas para um teto de 101 e dashfiz isso sem o subshell em 0,017 segundos e com o subshell em 1,8 segundos. busybox.149 e 2, zsh .149 e 4, bash.35 e 6 e ksh93em .149 e .160. ksh93não bifurca para subconchas como as outras conchas devem. Então, talvez o problema não seja tanto o subshell, mas também o shell .

mikeserv
fonte
Qual é a vantagem de [ "$((...))" -eq "$((...))" ]acabar (( (...) == (...) ))? O último é menos portátil?
ruach
@ruakh - portabilidade, velocidade, confiabilidade. [ "$((...))" -eq "$((...)) ]funciona em conchas que não levam 15 segundos para executar o programa, e o outro não. Se a vantagem de um sobre o outro é questionável, então isso só pode dar vantagem ao primeiro, o que significa que nunca há uma boa razão para usar (( (...) == (...) )).
mikeserv
Desculpe, mas sua resposta parece assumir que eu já tenho um conhecimento detalhado do suporte a shell (( ... )). Estou lisonjeado, mas eu não não tem esse conhecimento detalhado. (Lembre-se, sou eu quem apenas perguntei se (( ... ))é menos portátil.) Portanto, realmente não consigo entender sua resposta. : - / Você poderia ser um pouco mais explícito?
Ruakh
@ruakh - me desculpe ... eu não vi que você estava perguntando se era mais portátil, apenas como era vantajoso - e foi por isso que eu respondi sobre portabilidade. De qualquer forma, o POSIX"$((...))" é especificado e o outro é uma extensão do shell. Os shells POSIX são bastante capazes. Mesmo dashe poshirá lidar corretamente com testes de ramificação como "$((if_true ? (var=10) : (var=5) ))"e sempre atribuir $varcorretamente. busyboxquebra lá - sempre avalia ambos os lados, independentemente do $if_truevalor de.
mikeserv
@ruakh - oh cara. Hoje devo estar um pouco de folga ... diz ali mesmo ... o último é menos portátil ? Eu não vi isso antes, eu acho ...?
mikeserv