У Bash проблемы с производительностью при использовании списков аргументов?


11

Решено в bash 5.0

Фон

Для справки (и понимания (и попыток избежать отрицательных голосов этот вопрос, кажется, привлекает)) я объясню путь, который привел меня к этой проблеме (ну, лучшее, что я могу вспомнить через два месяца).

Предположим, вы выполняете некоторые тесты оболочки для списка символов Unicode:

printf "$(printf '\\U%x ' {33..200})"

и, учитывая, что в Unicode содержится более 1 миллиона символов, тестирование 20 000 из них выглядит не так уж и много.
Также предположим, что вы устанавливаете символы в качестве позиционных аргументов:

set -- $(printf "$(printf '\\U%x ' {33..20000})")

с целью передачи символов каждой функции, чтобы обрабатывать их по-разному. Таким образом, функции должны иметь форму test1 "$@"или подобное. Теперь я понимаю, как это плохо в bash.

Теперь предположим, что существует необходимость в времени (n = 1000) каждого решения, чтобы выяснить, что лучше, в таких условиях вы получите структуру, похожую на:

#!/bin/bash --
TIMEFORMAT='real: %R'  # '%R %U %S'

set -- $(printf "$(printf '\\U%x ' {33..20000})")
n=1000

test1(){ echo "$1"; } >/dev/null
test2(){ echo "$#"; } >/dev/null
test3(){ :; }

main1(){ time for i in $(seq $n); do test1 "$@"; done
         time for i in $(seq $n); do test2 "$@"; done
         time for i in $(seq $n); do test3 "$@"; done
       }

main1 "$@"

Функции test#сделаны очень очень просто, чтобы быть представленными здесь.
Оригиналы были постепенно урезаны, чтобы найти, где была огромная задержка.

Сценарий выше работает, вы можете запустить его и тратить несколько секунд, занимаясь совсем немного.

В процессе упрощения, чтобы точно определить, где была задержка (а сокращение многих тестовых функций почти до нуля является предельным после многих испытаний), я решил убрать передачу аргументов каждой тестовой функции, чтобы выяснить, насколько улучшилось время, только в 6 раз, не очень.

Чтобы попробовать себя, удалите все "$@"функции in main1(или сделайте копию) и протестируйте снова (или оба, main1и копию main2main2 "$@")) для сравнения. Это базовая структура ниже в исходном посте (ОП).

Но я задавался вопросом: почему оболочка так долго "ничего не делает"? Да только «пару секунд», но все же, зачем?

Это заставило меня проверить другие оболочки, чтобы обнаружить, что эта проблема была только у bash.
Попробуйте ksh ./script(тот же сценарий, что и выше).

Это приводит к этому описанию: вызов функции ( test#) без каких-либо аргументов задерживается аргументами в parent ( main#). Это описание, которое следует и было оригинальным постом (OP) ниже.

Оригинальный пост.

Вызов функции (в Bash 4.4.12 (1) -release), который ничего не делает f1(){ :; }, в тысячу раз медленнее, :но только если в родительской вызывающей функции определены аргументы. Почему?

#!/bin/bash
TIMEFORMAT='real: %R'

f1   () { :; }

f2   () {
   echo "                     args = $#";
   printf '1 function no   args yes '; time for ((i=1;i<$n;i++)); do  :   ; done 
   printf '2 function yes  args yes '; time for ((i=1;i<$n;i++)); do  f1  ; done
   set --
   printf '3 function yes  args no  '; time for ((i=1;i<$n;i++)); do  f1  ; done
   echo
        }

main1() { set -- $(seq $m)
          f2  ""
          f2 "$@"
        }

n=1000; m=20000; main1

Результаты test1:

                     args = 1
1 function no   args yes real:  0.013
2 function yes  args yes real:  0.024
3 function yes  args no  real:  0.020

                     args = 20000
1 function no   args yes real:  0.010
2 function yes  args yes real: 20.326
3 function yes  args no  real:  0.019

В функции нет ни аргументов, ни входных, ни выходных данных f1, задержка в тысячу (1000) раз неожиданна. 1


Если результаты тестов распространяются на несколько оболочек, результаты совпадают, большинство оболочек не испытывают никаких проблем и не страдают от задержек (используются те же n и m):

test2(){
          for sh in dash mksh ksh zsh bash b50sh
      do
          echo "$sh" >&2
#         \time -f '\t%E' seq "$m" >/dev/null
#         \time -f '\t%E' "$sh" -c 'set -- $(seq '"$m"'); for i do :; done'
          \time -f '\t%E' "$sh" -c 'f(){ :;}; while [ "$((i+=1))" -lt '"$n"' ]; do : ; done;' $(seq $m)
          \time -f '\t%E' "$sh" -c 'f(){ :;}; while [ "$((i+=1))" -lt '"$n"' ]; do f ; done;' $(seq $m)
      done
}

test2

Результаты:

dash
        0:00.01
        0:00.01
mksh
        0:00.01
        0:00.02
ksh
        0:00.01
        0:00.02
zsh
        0:00.02
        0:00.04
bash
        0:10.71
        0:30.03
b55sh             # --without-bash-malloc
        0:00.04
        0:17.11
b56sh             # RELSTATUS=release
        0:00.03
        0:15.47
b50sh             # Debug enabled (RELSTATUS=alpha)
        0:04.62
        xxxxxxx    More than a day ......

Раскомментируйте два других теста, чтобы подтвердить, что ни seqзадержка , ни обработка списка аргументов не являются источником.

1 Этоизвестночто передача результатов аргументов увеличивает время выполнения. Спасибо@slm


3
Сохранено мета-эффектом. unix.meta.stackexchange.com/q/5021/3562
Джошуа

Ответы:


9

Скопировано из: Почему задержка в цикле? по вашему запросу:

Вы можете сократить контрольный пример до:

time bash -c 'f(){ :;};for i do f; done' {0..10000}

Он вызывает функцию, когда $@она велика, что, кажется, вызывает ее.

Я предполагаю, что время тратится на сохранение $@в стеке и его восстановление впоследствии. Возможно, bashделает это очень неэффективно, дублируя все значения или что-то в этом роде. Кажется, время в o (n²).

Вы получаете такое же время в других оболочках для:

time zsh -c 'f(){ :;};for i do f "$@"; done' {0..10000}

Именно здесь вы передаете список аргументов функциям, и на этот раз оболочка должна скопировать значения (в bashконечном итоге это в 5 раз медленнее).

(Сначала я думал, что это хуже в bash 5 (в настоящее время в альфа-версии), но это сводилось к тому, что отладка malloc была включена в версиях разработки, как отмечает @egmont; также проверьте, как собирается ваш дистрибутив, bashесли вы хотите сравнить свою собственную сборку с система одна. Например, Ubuntu использует --without-bash-malloc)


Как убрана отладка?
NotAnUnixNazi

@isaac, я сделал это, изменив RELSTATUS=alphaк RELSTATUS=releaseв configureсценарии.
Стефан Шазелас

Добавлены результаты теста для обоих --without-bash-mallocи RELSTATUS=releaseк результатам вопроса. Это все еще показывает проблему с вызовом ф.
NotAnUnixNazi

@ Исаак, да, я только что сказал, что ошибался, говоря, что в bash5 было хуже. Это не хуже, это так же плохо.
Стефан Шазелас

Нет, это не так плохо . Bash5 решает проблему с вызовом :и немного улучшает вызов f. Посмотрите на сроки test2 в вопросе.
NotAnUnixNazi
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.