Почему использование && в 75 раз быстрее, чем… fi, и как сделать код понятнее

38

У меня есть следующий рабочий код:

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"

Этот код работает быстро составляет 0,194 секунды. Тем не менее, я обнаружил, что && is_prime= falseтекст немного сложен для чтения, и это может показаться (неопытному глазу), как будто его тестируют, а не устанавливают, что и делает. Так что я попытался изменить &&в if...thenи это работает - но в 75 раз медленнее на 14,48 секунд. Это наиболее заметно на старших цифрах.

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"

Есть ли какая-то четкость блока без медлительности?

Обновление (04.01.2015, 10:40 EST)

Отличная обратная связь! Я сейчас использую следующее. Другие отзывы ?

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
На sidenote, запуск вашего скрипта распечатывает это Largest Prime= 100на моем компьютере.
Джулио Мускарелло
3
Кроме того, если вы заинтересованы в эффективности, одним из тривиальных способов улучшить это было бы повторение только до, number_under_test/2а не до number_under_test-1: ни один из коэффициентов числа n не больше n / 2, поэтому вы все равно найдете все факторы для не простых чисел, делая это. (Также, если вас интересует только проверка на простоту, было бы достаточно итерировать до sqrt (n), но Bash в любом случае не имеет встроенной функции для вычисления квадратных корней.)
Malte Skoruppa
Матовый, хорошая точка (+1). Единственное изменение состояло в том, что это не сработало для номера 4, поэтому я должен был сделать это, (number_under_test/2)+1чтобы учесть это
Майкл Даррант
1
В вашей обновленной версии, брекеты {}не очень нужны после thenпункта , потому что thenуже служит в качестве группировки оператора (наряду с elif, elseили fi). На самом деле, в некоторых оболочках вы можете написать, например, for i in 1 2 3; { echo $i; }без doили done.
Джонатан Леффлер
1
+1 Джонатан, я внес эти изменения и обновил обновление
Майкл Даррант

Ответы:

66

Это потому, что вы каждый раз создаете суб-оболочку:

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

Просто удалите скобки

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

Если вы хотите сгруппировать команды, в текущей оболочке для этого есть синтаксис :

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

(конечная точка с запятой требуется, см. руководство )

Обратите внимание, что [ is_prime ]это не то же самое, что [ $is_prime == true ]: вы могли бы написать это так же просто $is_prime(без скобок), которое вызвало бы встроенную команду trueили falseкоманду bash .
[ is_prime ]это тест с одним аргументом, строка «is_prime» - когда [задан один аргумент, результатом является успех, если аргумент не пустой, и эта литеральная строка всегда непуста, следовательно, всегда «true».

Для удобства чтения я бы поменял очень длинную строку

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

Не недооценивайте пробелы, чтобы улучшить ясность.

Гленн Джекман
источник
1
есть опечатка - largest_prime=$number_under_testдолжна быть в тогдашней ветке (та же ошибка в оригинале)
JJoao
1
Также стоит отметить, что в bash zsh и др. [Вызывает программу с буквальным названием [, в то время как [[она реализована в оболочке - следовательно, это будет быстрее. Попробуйте time for ((i = 0; $i < 1000; i++)); do [ 1 ]; doneи сравните с [[. Смотрите этот ТАК вопрос для получения дополнительной информации.
Кирб
2
Bash реализует [, это встроенный. В командной строке введите type -a [иhelp [
glenn jackman
@glennjackman Wow; не знал об этом. Я предположил, что это все еще так, потому что which [все еще возвращается /usr/bin/[. Я также только что понял, что подразумевал, что zsh был тем же самым; для меня это говорит мне, что это встроенный. Но тогда ... почему [[быстрее?
Кирб
2
@glennjackman command -v- еще одна хорошая whichальтернатива; Смотрите также здесь .
Аббафей
9

Я думаю, вы слишком усердно работаете над своей функцией. Рассмотреть возможность:

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

Оболочечная арифметика довольно способна самостоятельно оценивать целочисленные условия. Это редко требует слишком много тестов и / или внешних заданий. Этот whileцикл хорошо дублирует ваши вложенные циклы:

Конечно, он печатает не так много, я не так много писал, но, например, установил потолок 16, а не 101, как написано выше, и ...

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

Это определенно делает работу. И для аппроксимации вашего результата требуется совсем немного:

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

Просто делаю это, а не 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

Это работает в busybox. Это очень портативный, быстрый и простой в использовании.

Ваша проблема с подоболочками возникает в большинстве оболочек, но, безусловно , наиболее остро в bashоболочке. Я чередовал

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

... и как я написал выше в нескольких оболочках для потолка 101 и dashсделал это без вложенной оболочки в 0,017 секунды и с вложенной оболочкой в ​​1,8 секунды. busybox.149 и 2, зш .149 и 4, bash.35 и 6, и ksh93в .149 и .160. ksh93не разветвляется на подоболочки, как другие оболочки. Так что, возможно, проблема не столько в оболочке, сколько в оболочке .

mikeserv
источник
Что преимущество [ "$((...))" -eq "$((...))" ]над (( (...) == (...) ))? Последний менее портативный?
Руах
@ruakh - мобильность, скорость, надежность. [ "$((...))" -eq "$((...)) ]работает в оболочках, которым не требуется 15 секунд для запуска программы, а другой нет. Если преимущество одного над другим вообще сомнительно, то это может только дать преимущество первому, что означает, что никогда не будет веской причины для использования (( (...) == (...) )).
mikeserv
Извините, но ваш ответ, похоже, предполагает, что у меня уже есть подробные знания о поддержке оболочки (( ... )). Я польщен, но у меня нет таких подробных знаний. (Помните, я тот, кто только что спросил, (( ... ))менее ли переносим.) Поэтому я действительно не могу понять ваш ответ. : - / Не могли бы вы быть немного более явным?
Руах
@ruakh - Извини ... Я не видел, чтобы ты спрашивал, был ли он более переносимым, насколько он был выгоден - вот почему я ответил о переносимости. В любом случае, "$((...))"это POSIX-спецификация, а другое расширение оболочки. Оболочки POSIX вполне способны. Даже dashи poshбудет правильно обрабатывать отраслевые тесты, как "$((if_true ? (var=10) : (var=5) ))"и всегда назначать $varправильно. busyboxломается там - это всегда уносит обе стороны независимо от российской $if_trueценности.
mikeserv
@ruakh - о боже. Я, должно быть, сегодня немного оторвался ... тут написано ... последний менее переносим ? Я не видел этого раньше, я думаю ...?
mikeserv