Apakah pemipaan, pemindahan, atau perluasan parameter lebih efisien?

26

Saya mencoba menemukan cara paling efisien untuk beralih melalui nilai-nilai tertentu yang merupakan jumlah nilai yang konsisten dari satu sama lain dalam daftar kata yang dipisahkan oleh ruang (saya tidak ingin menggunakan array). Sebagai contoh,

list="1 ant bat 5 cat dingo 6 emu fish 9 gecko hare 15 i j"

Jadi saya ingin bisa beralih melalui daftar dan hanya mengakses 1,5,6,9 dan 15.

EDIT: Seharusnya saya menjelaskan bahwa nilai yang saya coba dapatkan dari daftar tidak harus berbeda dalam format dari sisa daftar. Apa yang membuat mereka istimewa adalah semata-mata posisi mereka dalam daftar (Dalam hal ini, posisi 1,4,7 ...). Jadi daftarnya bisa saja1 2 3 5 9 8 6 90 84 9 3 2 15 75 55tetapi saya masih menginginkan angka yang sama. Dan juga, saya ingin dapat melakukannya dengan asumsi saya tidak tahu panjang daftar.

Metode yang saya pikirkan sejauh ini adalah:

Metode 1

set $list
found=false
find=9
count=1
while [ $count -lt $# ]; do
    if [ "${@:count:1}" -eq $find ]; then
    found=true
    break
    fi
    count=`expr $count + 3`
done

Metode 2

set list
found=false
find=9
while [ $# ne 0 ]; do
    if [ $1 -eq $find ]; then
    found=true
    break
    fi
    shift 3
done

Metode 3 Saya cukup yakin perpipaan membuat ini pilihan terburuk, tetapi saya mencoba menemukan metode yang tidak menggunakan set, karena penasaran.

found=false
find=9
count=1
num=`echo $list | cut -d ' ' -f$count`
while [ -n "$num" ]; do
    if [ $num -eq $find ]; then
    found=true
    break
    fi
    count=`expr $count + 3`
    num=`echo $list | cut -d ' ' -f$count`
done

Jadi apa yang paling efisien, atau saya melewatkan metode yang lebih sederhana?

Levi Uzodike
sumber
10
Saya tidak akan menggunakan skrip shell di tempat pertama jika efisiensi merupakan masalah penting. Seberapa besar daftar Anda sehingga membuat perbedaan?
Barmar
2
Tanpa melakukan statistik atas contoh aktual masalah Anda, Anda tidak akan tahu apa-apa. Ini termasuk membandingkan dengan "pemrograman dalam awk" dll. Jika statistik terlalu mahal, maka mencari efisiensi mungkin tidak sepadan.
David Tonhofer
2
Levi, apa sebenarnya cara "efisien" dalam definisi Anda? Anda ingin menemukan cara yang lebih cepat untuk beralih?
Sergiy Kolodyazhnyy

Jawaban:

18

Cukup sederhana awk. Ini akan memberi Anda nilai setiap bidang keempat untuk input dengan panjang berapa pun:

$ awk -F' ' '{for( i=1;i<=NF;i+=3) { printf( "%s%s", $i, OFS ) }; printf( "\n" ) }' <<< $list
1 5 6 9 15

Ini berfungsi memanfaatkan awkvariabel bawaan seperti NF(jumlah bidang dalam catatan), dan melakukan beberapa forperulangan sederhana untuk beralih di sepanjang bidang untuk memberi Anda yang Anda inginkan tanpa perlu tahu sebelumnya berapa banyak akan ada.

Atau, jika Anda memang hanya ingin bidang-bidang tertentu seperti yang ditentukan dalam contoh Anda:

$ awk -F' ' '{ print $1, $4, $7, $10, $13 }' <<< $list
1 5 6 9 15

Adapun pertanyaan tentang efisiensi, rute paling sederhana adalah dengan menguji ini atau masing-masing metode Anda yang lain dan gunakan timeuntuk menunjukkan berapa lama waktu yang dibutuhkan; Anda juga bisa menggunakan alat seperti straceuntuk melihat bagaimana sistem panggilan mengalir. Penggunaan timeterlihat seperti:

$ time ./script.sh

real    0m0.025s
user    0m0.004s
sys     0m0.008s

Anda dapat membandingkan output tersebut antara berbagai metode untuk melihat mana yang paling efisien dalam hal waktu; alat lain dapat digunakan untuk metrik efisiensi lainnya.

DopeGhoti
sumber
1
Poin bagus, @MichaelHomer; Saya telah menambahkan kata sampingan yang menjawab pertanyaan "bagaimana saya bisa menentukan metode mana yang paling efisien ".
DopeGhoti
2
@LeviUzodike Mengenai echovs <<<, "identik" adalah kata yang terlalu kuat. Bisa dibilang stuff <<< "$list"hampir identik dengan printf "%s\n" "$list" | stuff. Mengenai echovs printf, saya mengarahkan Anda ke jawaban ini
JoL
5
@DopeGhoti Sebenarnya begitu. <<<menambahkan baris baru di akhir. Ini mirip dengan cara $()menghapus baris baru dari akhir. Ini karena baris diakhiri oleh baris baru. <<<mengumpankan ekspresi sebagai garis, sehingga harus diakhiri oleh baris baru. "$()"mengambil baris dan menyediakannya sebagai argumen, jadi masuk akal untuk mengonversi dengan menghapus baris baru yang berhenti.
JoL
3
@LeviUzodike awk adalah alat yang sangat tidak dihargai. Ini akan membuat segala macam masalah yang tampaknya rumit mudah diselesaikan. Terutama ketika Anda mencoba untuk menulis regex yang kompleks untuk sesuatu seperti sed, Anda sering dapat menghemat waktu dengan menulisnya secara prosedural dalam awk. Mempelajari hal itu akan menghasilkan dividen yang besar.
Joe
1
@LeviUzodike: Ya awkadalah biner yang berdiri sendiri yang harus memulai. Tidak seperti perl atau terutama Python, interpreter awk memulai dengan cepat (masih semua overhead linker dinamis yang biasa membuat cukup banyak panggilan sistem, tetapi awk hanya menggunakan libc / libm dan libdl. Mis. Gunakan straceuntuk memeriksa panggilan sistem awk startup awk) . Banyak shell (seperti bash) sangat lambat, sehingga menjalankan satu proses awk bisa lebih cepat daripada melompati token dalam daftar dengan built-in shell bahkan untuk ukuran daftar ish kecil. Dan terkadang Anda bisa menulis #!/usr/bin/awkskrip alih - alih#!/bin/sh skrip.
Peter Cordes
35
  • Aturan pertama optimasi perangkat lunak: Jangan .

    Sampai Anda tahu kecepatan program adalah masalah, Anda tidak perlu memikirkan seberapa cepatnya. Jika daftar Anda panjangnya atau hanya ~ 100-1000 item, Anda mungkin tidak akan menyadari berapa lama. Ada kemungkinan Anda menghabiskan lebih banyak waktu untuk memikirkan pengoptimalan daripada apa perbedaannya.

  • Aturan kedua: Ukur .

    Itu cara yang pasti untuk mencari tahu, dan yang memberi jawaban untuk sistem Anda. Terutama dengan kerang, ada begitu banyak, dan mereka tidak semuanya identik. Jawaban untuk satu shell mungkin tidak berlaku untuk Anda.

    Dalam program yang lebih besar, pembuatan profil juga dilakukan di sini. Bagian paling lambat mungkin bukan yang Anda pikirkan.

  • Ketiga, aturan pertama optimasi skrip shell: Jangan gunakan shell .

    Ya benar Banyak shell tidak dibuat menjadi cepat (karena meluncurkan program eksternal tidak harus), dan mereka bahkan dapat mengurai baris kode sumber lagi setiap kali.

    Gunakan sesuatu seperti awk atau Perl sebagai gantinya. Dalam patokan mikro sepele yang saya lakukan,awk adalah puluhan kali lebih cepat daripada shell umum dalam menjalankan loop sederhana (tanpa I / O).

    Namun, jika Anda menggunakan shell, gunakan fungsi builtin shell bukan perintah eksternal. Di sini, Anda menggunakan expryang tidak dibangun di shell yang saya temukan di sistem saya, tetapi yang dapat diganti dengan ekspansi aritmatika standar. Misalnya i=$((i+1))alih-alih i=$(expr $i + 1)menambah i. Penggunaan Anda cutdalam contoh terakhir mungkin juga dapat diganti dengan ekspansi parameter standar.

    Lihat juga: Mengapa menggunakan shell loop untuk memproses teks dianggap praktik buruk?

Langkah # 1 dan # 2 seharusnya berlaku untuk pertanyaan Anda.

ilkkachu
sumber
12
# 0, kutip ekspansi Anda :-)
Kusalananda
8
Bukan berarti awkloop selalu lebih baik atau lebih buruk daripada loop shell. Shell itu sangat bagus dalam menjalankan perintah dan mengarahkan input dan output ke dan dari proses, dan terus terang agak kikuk di segala hal lain; sementara alat-alat seperti awkyang fantastis di pengolahan data teks, karena itu apa kerang dan alat-alat seperti awkyang dibuat untuk (masing-masing) di tempat pertama.
DopeGhoti
2
@DopeGhoti, kerang tampaknya lebih lambat secara objektif. Beberapa loop yang sangat sederhana tampaknya> 25 kali lebih lambat dashdaripada dengan gawk, dan dashmerupakan shell tercepat yang saya uji ...
ilkkachu
1
@ Jo, itu :) dashdan busyboxtidak mendukung (( .. ))- Saya pikir ini adalah ekstensi yang tidak standar. ++juga secara eksplisit disebutkan sebagai tidak wajib, sejauh yang saya tahu, i=$((i+1))atau : $(( i += 1))yang aman.
ilkkachu
1
Re "lebih banyak waktu berpikir" : ini mengabaikan faktor penting. Seberapa sering dijalankan, dan untuk berapa banyak pengguna? Jika sebuah program membuang 1 detik, yang bisa diperbaiki oleh programmer memikirkannya selama 30 menit, itu mungkin buang-buang waktu jika hanya ada satu pengguna yang akan menjalankannya sekali. Di sisi lain jika ada satu juta pengguna, itu sejuta detik, atau 11 hari dari waktu pengguna. Jika kodenya menghabiskan satu menit dari satu juta pengguna, itu sekitar 2 tahun dari waktu pengguna.
AGC
13

Saya hanya akan memberikan beberapa saran umum dalam jawaban ini, dan bukan tolok ukur. Benchmark adalah satu-satunya cara untuk secara andal menjawab pertanyaan tentang kinerja. Tetapi karena Anda tidak mengatakan berapa banyak data yang Anda manipulasi dan seberapa sering Anda melakukan operasi ini, tidak ada cara untuk melakukan benchmark yang berguna. Apa yang lebih efisien untuk 10 item dan apa yang lebih efisien untuk 1000000 item seringkali tidak sama.

Sebagai aturan umum, menjalankan perintah eksternal lebih mahal daripada melakukan sesuatu dengan konstruksi shell murni, selama kode shell murni tidak melibatkan loop. Di sisi lain, perulangan shell yang beriterasi pada string besar atau sejumlah besar string cenderung lebih lambat dari satu permintaan alat tujuan khusus. Misalnya, menjalankan loop Anda cutbisa terasa lambat dalam praktik, tetapi jika Anda menemukan cara untuk melakukan semuanya dengan satucut pemanggilan yang mungkin lebih cepat daripada melakukan hal yang sama dengan manipulasi string dalam shell.

Perhatikan bahwa titik batas dapat sangat bervariasi antar sistem. Itu dapat bergantung pada kernel, pada bagaimana penjadwal kernel dikonfigurasikan, pada sistem file yang berisi executable eksternal, pada seberapa banyak tekanan CPU vs memori yang ada saat ini, dan banyak faktor lainnya.

Jangan menelepon expruntuk melakukan aritmatika jika Anda benar-benar khawatir tentang kinerja. Bahkan, jangan menelepon expruntuk melakukan aritmatika sama sekali. Kerang memiliki aritmatika bawaan, yang lebih jelas dan lebih cepat daripada memohon expr.

Anda tampaknya menggunakan bash, karena Anda menggunakan konstruksi bash yang tidak ada di sh. Jadi mengapa Anda tidak menggunakan array? Array adalah solusi yang paling alami, dan kemungkinan akan menjadi yang tercepat juga. Perhatikan bahwa indeks array mulai dari 0.

list=(1 2 3 5 9 8 6 90 84 9 3 2 15 75 55)
for ((count = 0; count += 3; count < ${#list[@]})); do
  echo "${list[$count]}"
done

Skrip Anda mungkin lebih cepat jika Anda menggunakan sh, jika sistem Anda memiliki tanda hubung atau ksh shdaripada bash. Jika Anda menggunakan sh, Anda tidak mendapatkan array bernama, tetapi Anda masih mendapatkan array salah satu parameter posisi, yang dapat Anda atur set. Untuk mengakses elemen pada posisi yang tidak diketahui sampai runtime, Anda perlu menggunakan eval(berhati-hatilah mengutip sesuatu dengan benar!).

# List elements must not contain whitespace or ?*\[
list='1 2 3 5 9 8 6 90 84 9 3 2 15 75 55'
set $list
count=1
while [ $count -le $# ]; do
  eval "value=\${$count}"
  echo "$value"
  count=$((count+1))
done

Jika Anda hanya ingin mengakses array sekali dan bergerak dari kiri ke kanan (melewatkan beberapa nilai), Anda dapat menggunakan shiftsebagai ganti indeks variabel.

# List elements must not contain whitespace or ?*\[
list='1 2 3 5 9 8 6 90 84 9 3 2 15 75 55'
set $list
while [ $# -ge 1 ]; do
  echo "$1"
  shift && shift && shift
done

Pendekatan mana yang lebih cepat tergantung pada shell dan jumlah elemen.

Kemungkinan lain adalah menggunakan pemrosesan string. Ini memiliki keuntungan karena tidak menggunakan parameter posisi, sehingga Anda dapat menggunakannya untuk hal lain. Ini akan lebih lambat untuk sejumlah besar data, tetapi itu tidak mungkin membuat perbedaan nyata untuk sejumlah kecil data.

# List elements must be separated by a single space (not arbitrary whitespace)
list='1 2 3 5 9 8 6 90 84 9 3 2 15 75 55'
while [ -n "$list" ]; do
  echo "${list% *}"
  case "$list" in *\ *\ *\ *) :;; *) break;; esac
  list="${list#* * * }"
done
Gilles 'SANGAT berhenti menjadi jahat'
sumber
" Di sisi lain, sebuah loop shell yang beriterasi pada string besar atau sejumlah besar string cenderung lebih lambat dari satu permintaan alat tujuan khusus " tetapi bagaimana jika alat itu memiliki loop di dalamnya seperti awk? @ikkachu mengatakan awk loop lebih cepat, tetapi apakah Anda akan mengatakan bahwa dengan <1000 field untuk diulangi, manfaat loop lebih cepat tidak akan lebih besar daripada biaya memanggil awk karena ini adalah perintah eksternal (dengan asumsi saya bisa melakukan tugas yang sama di shell loop dengan penggunaan hanya perintah bawaan)?
Levi Uzodike
@LeviUzodike Harap baca ulang paragraf pertama jawaban saya.
Gilles 'SO- stop being evil'
Anda juga bisa mengganti shift && shift && shiftdengan shift 3dalam contoh ketiga Anda - kecuali shell yang Anda gunakan tidak mendukungnya.
Joe
2
@ Jo Sebenarnya, tidak. shift 3akan gagal jika ada terlalu sedikit argumen yang tersisa. Anda perlu sesuatu sepertiif [ $# -gt 3 ]; then shift 3; else set --; fi
Gilles 'SO- stop being evil'
3

awkadalah pilihan yang bagus, jika Anda dapat melakukan semua pemrosesan di dalam skrip Awk. Jika tidak, Anda hanya akan mengirim output Awk ke utilitas lain, menghancurkan perolehan kinerja awk.

bashiterasi atas array juga bagus, jika Anda bisa memasukkan seluruh daftar Anda ke dalam array (yang untuk shell modern mungkin merupakan jaminan) dan Anda tidak keberatan dengan senam sintaks array.

Namun, pendekatan saluran pipa:

xargs -n3 <<< "$list" | while read -ra a; do echo $a; done | grep 9

Dimana:

  • xargs mengelompokkan daftar yang dipisahkan spasi menjadi tiga, masing-masing baris baru dipisahkan
  • while read mengkonsumsi daftar itu dan menampilkan kolom pertama dari setiap grup
  • grep memfilter kolom pertama (sesuai dengan setiap posisi ketiga dalam daftar asli)

Meningkatkan pemahaman, menurut saya. Orang-orang sudah tahu apa yang dilakukan alat-alat ini, sehingga mudah dibaca dari kiri ke kanan dan alasan tentang apa yang akan terjadi. Pendekatan ini juga secara jelas mendokumentasikan panjang langkah ( -n3) dan pola filter ( 9), sehingga mudah untuk membuat variasi:

count=3
find=9
xargs -n "$count" <<< "$list" | while read -ra a; do echo $a; done | grep "$find"

Ketika kita mengajukan pertanyaan "efisiensi", pastikan untuk memikirkan "efisiensi seumur hidup total". Perhitungan itu mencakup upaya pengelola untuk menjaga agar kode tetap berfungsi, dan kami, kantung daging adalah mesin yang paling tidak efisien di seluruh operasi.

uskup
sumber
2

Mungkin ini

cut -d' ' -f1,4,7,10,13 <<<$list
1 5 6 9 15
selesai24
sumber
Maaf saya tidak jelas sebelumnya, tetapi saya ingin bisa mendapatkan angka di posisi itu tanpa mengetahui panjang daftar. Tapi terima kasih, aku lupa memotong bisa melakukan itu.
Levi Uzodike
1

Jangan gunakan perintah shell jika Anda ingin menjadi efisien. Batasi diri Anda untuk pipa, pengalihan, pergantian dll, dan program. Itu sebabnya xargsdan parallelutilitas ada - karena bash sementara loop tidak efisien dan sangat lambat. Gunakan bash loop hanya sebagai resolusi terakhir.

list="1 ant bat 5 cat dingo 6 emu fish 9 gecko hare 15 i j"
if 
    <<<"$list" tr -d -s '[0-9 ]' | 
    tr -s ' ' | tr ' ' '\n' | 
    grep -q -x '9'
then
    found=true
else 
    found=false
fi
echo ${found} 

Tapi Anda mungkin harus agak lebih cepat dengan yang baik awk.

KamilCuk
sumber
Maaf saya tidak jelas sebelumnya, tetapi saya sedang mencari solusi yang dapat mengekstraksi nilai-nilai hanya berdasarkan posisi mereka dalam daftar. Saya hanya membuat daftar asli seperti itu karena saya ingin itu menjadi jelas nilai-nilai yang saya inginkan.
Levi Uzodike
1

Menurut pendapat saya solusi yang paling jelas (dan mungkin yang paling performan juga) adalah dengan menggunakan variabel awk RS dan ORS:

awk -v RS=' ' -v ORS=' ' 'NR % 3 == 1' <<< "$list"
pengguna000001
sumber
1
  1. Menggunakan skrip shell GNU sed dan POSIX :

    echo $(printf '%s\n' $list | sed -n '1~3p')
  2. Atau dengan bash's substitusi parameter :

    echo $(sed -n '1~3p' <<< ${list// /$'\n'})
  3. Non- GNU ( yaitu POSIX ) sed, dan bash:

    sed 's/\([^ ]* \)[^ ]* *[^ ]* */\1/g' <<< "$list"

    Atau lebih mudah dibawa, menggunakan POSIX sed dan skrip shell:

    echo "$list" | sed 's/\([^ ]* \)[^ ]* *[^ ]* */\1/g'

Output dari semua ini:

1 5 6 9 15
agc
sumber