Bash memiliki masalah kinerja menggunakan daftar argumen?

11

Diselesaikan di bash 5.0

Latar Belakang

Untuk latar belakang (dan pemahaman (dan mencoba untuk menghindari downvotes pertanyaan ini tampaknya menarik)) Saya akan menjelaskan jalan yang membawa saya ke masalah ini (well, yang terbaik yang bisa saya ingat dua bulan kemudian).

Asumsikan Anda sedang melakukan beberapa tes shell untuk daftar karakter Unicode:

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

dan ada lebih dari 1 juta karakter Unicode, pengujian 20.000 dari mereka tampaknya tidak banyak.
Juga asumsikan bahwa Anda menetapkan karakter sebagai argumen posisi:

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

dengan maksud meneruskan karakter ke setiap fungsi untuk memprosesnya dengan cara yang berbeda. Jadi fungsinya harus berbentuk test1 "$@"atau mirip. Sekarang saya menyadari betapa buruknya ide ini di bash.

Sekarang, asumsikan bahwa ada waktu (n = 1000) setiap solusi untuk mencari tahu mana yang lebih baik, dalam kondisi seperti itu Anda akan berakhir dengan struktur yang mirip dengan:

#!/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 "$@"

Fungsinya test#dibuat sangat sederhana hanya untuk disajikan di sini.
Dokumen aslinya dipangkas secara progresif untuk menemukan di mana penundaan yang sangat besar.

Script di atas berfungsi, Anda dapat menjalankannya dan menghabiskan beberapa detik dengan sangat sedikit.

Dalam proses penyederhanaan untuk menemukan dengan tepat di mana penundaan itu (dan mengurangi setiap fungsi tes menjadi hampir tidak ada yang ekstrim setelah banyak uji coba) saya memutuskan untuk menghapus berlalunya argumen ke setiap fungsi tes untuk mengetahui berapa banyak waktu meningkat, hanya saja faktor 6, tidak banyak.

Untuk mencoba sendiri, hapus semua "$@"fungsi dalam main1(atau buat salinan) dan uji lagi (atau keduanya main1dan salin main2(dengan main2 "$@")) untuk membandingkan. Ini adalah struktur dasar di bawah di pos asli (OP).

Tetapi saya bertanya-tanya: mengapa cangkang itu membutuhkan waktu lama untuk "tidak melakukan apa-apa"? Ya, hanya "beberapa detik", tetapi tetap saja, mengapa?

Ini membuat saya menguji di shell lain untuk menemukan bahwa hanya bash yang memiliki masalah ini.
Coba ksh ./script(skrip yang sama seperti di atas).

Ini mengarah ke deskripsi ini: memanggil fungsi ( test#) tanpa argumen akan ditunda oleh argumen di induk ( main#). Ini adalah deskripsi yang mengikuti dan merupakan pos asli (OP) di bawah ini.

Pos asli.

Memanggil fungsi (dalam Bash 4.4.12 (1) -release) untuk tidak melakukan apa-apa f1(){ :; }adalah seribu kali lebih lambat daripada :tetapi hanya jika ada argumen yang didefinisikan dalam fungsi panggilan induk , Mengapa?

#!/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

Hasil dari 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

Tidak ada argumen atau input atau output yang digunakan dalam fungsi f1, penundaan faktor seribu (1000) tidak terduga. 1


Memperluas tes ke beberapa cangkang, hasilnya konsisten, sebagian besar cangkang tidak memiliki masalah atau menderita keterlambatan (n dan m yang sama digunakan):

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

Hasil:

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 ......

Batalkan komentar dua tes lainnya untuk mengonfirmasi bahwa baik seqatau memproses daftar argumen adalah sumber untuk keterlambatan.

1 Halini diketahui bahwa lewat hasil dengan argumen akan meningkatkan waktu eksekusi. Terima kasih@slm

NotAnUnixNazi
sumber
3
Disimpan oleh efek meta. unix.meta.stackexchange.com/q/5021/3562
Joshua

Jawaban:

9

Disalin dari: Mengapa penundaan dalam loop? atas permintaan Anda:

Anda dapat mempersingkat test case ke:

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

Itu memanggil fungsi sementara $@besar yang tampaknya memicu itu.

Dugaan saya adalah bahwa waktu dihabiskan menabung $@ke tumpukan dan mengembalikannya sesudahnya. Mungkin bashmelakukannya dengan sangat tidak efisien dengan menduplikasi semua nilai atau sesuatu seperti itu. Waktu tampaknya dalam o (n²).

Anda mendapatkan jenis waktu yang sama di kulit lain untuk:

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

Di situlah Anda melewati daftar argumen ke fungsi, dan kali ini shell perlu menyalin nilai-nilai ( bashakhirnya menjadi 5 kali lebih lambat untuk yang itu).

(Awalnya saya pikir itu lebih buruk di bash 5 (saat ini dalam alpha), tapi itu ke debug malloc diaktifkan dalam versi pengembangan seperti dicatat oleh @egmont; juga memeriksa bagaimana distribusi Anda membangun bashjika Anda ingin membandingkan bangunan Anda sendiri dengan satu sistem. Misalnya, Ubuntu menggunakan --without-bash-malloc)

Stéphane Chazelas
sumber
Bagaimana cara debugging dihapus?
NotAnUnixNazi
@isaac, saya melakukannya dengan mengubah RELSTATUS=alphake RELSTATUS=releasedalam configureskrip.
Stéphane Chazelas
Menambahkan hasil tes untuk kedua --without-bash-mallocdan RELSTATUS=releaseke hasil pertanyaan. Itu masih menunjukkan masalah dengan panggilan ke f.
NotAnUnixNazi
@ Isaac, ya, saya hanya mengatakan saya dulu salah mengatakan bahwa itu lebih buruk di bash5. Itu tidak lebih buruk, sama buruknya.
Stéphane Chazelas
Tidak, tidak seburuk itu . Bash5 memecahkan masalah dengan memanggil :dan meningkatkan sedikit pada panggilan f. Lihatlah timing test2 dalam pertanyaan.
NotAnUnixNazi