Le tubazioni, lo spostamento o l'espansione dei parametri sono più efficienti?


26

Sto cercando di trovare il modo più efficiente di scorrere attraverso alcuni valori che sono un numero consistente di valori distanti tra loro in un elenco di parole separato da spazi (non voglio usare un array). Per esempio,

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

Quindi voglio essere in grado di scorrere semplicemente l'elenco e accedere solo a 1,5,6,9 e 15.

EDIT: avrei dovuto chiarire che i valori che sto cercando di ottenere dall'elenco non devono essere di formato diverso dal resto dell'elenco. Ciò che li rende speciali è solo la loro posizione nell'elenco (in questo caso, la posizione 1,4,7 ...). Quindi l'elenco potrebbe essere,1 2 3 5 9 8 6 90 84 9 3 2 15 75 55ma vorrei comunque gli stessi numeri. Inoltre, voglio essere in grado di farlo supponendo che non conosca la lunghezza dell'elenco.

I metodi che ho pensato finora sono:

Metodo 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

Metodo 2

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

Metodo 3 Sono abbastanza sicuro che il piping renda questa l'opzione peggiore, ma stavo cercando di trovare un metodo che non usi set, per curiosità.

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

Quindi quale sarebbe il più efficiente o mi manca un metodo più semplice?


10
Non userei uno script di shell in primo luogo se l'efficienza è una preoccupazione importante. Quanto è grande il tuo elenco che fa la differenza?
Barmar


2
Senza fare statistiche su casi reali del tuo problema, non saprai nulla. Ciò include il confronto con la "programmazione in awk" ecc. Se le statistiche sono troppo costose, probabilmente non vale la pena cercare l'efficienza.
David Tonhofer,

2
Levi, qual è esattamente il modo "efficiente" nella tua definizione? Vuoi trovare un modo più veloce per iterare?
Sergiy Kolodyazhnyy il

Risposte:


18

Abbastanza semplice con awk. Questo ti darà il valore di ogni quarto campo per input di qualsiasi lunghezza:

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

Questo funziona sfruttando le awkvariabili incorporate come NF(il numero di campi nel record) e facendo un semplice forciclo per iterare lungo i campi per darti quelli che vuoi senza dover sapere in anticipo quanti ce ne saranno.

Oppure, se vuoi davvero quei campi specifici come specificato nel tuo esempio:

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

Per quanto riguarda la domanda sull'efficienza, il percorso più semplice sarebbe quello di testare questo o ciascuno dei tuoi altri metodi e utilizzare timeper mostrare quanto tempo ci vuole; puoi anche usare strumenti come straceper vedere come il flusso di chiamate di sistema. L'utilizzo di timeassomiglia a:

$ time ./script.sh

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

Puoi confrontare quell'output tra vari metodi per vedere qual è il più efficiente in termini di tempo; altri strumenti possono essere utilizzati per altre metriche di efficienza.


1
Buon punto, @MichaelHomer; Ho aggiunto una parte che affronta la domanda "come posso determinare quale metodo è il più efficiente ".
DopeGhoti,

2
@LeviUzodike Per quanto riguarda echovs <<<, "identico" è una parola troppo forte. Si potrebbe dire che stuff <<< "$list"è quasi identico a printf "%s\n" "$list" | stuff. Riguardo al echovs printf, ti indirizzo a questa risposta
JoL

5
@DopeGhoti In realtà lo fa. <<<aggiunge una nuova riga alla fine. Questo è simile a come $()rimuove una nuova riga dalla fine. Questo perché le linee sono terminate da newline. <<<alimenta un'espressione come una linea, quindi deve essere terminata da una nuova riga. "$()"prende le linee e le fornisce come argomento, quindi ha senso convertirle rimuovendo la nuova riga che termina.
JoL

3
@LeviUzodike awk è uno strumento molto poco apprezzato. Renderà facile risolvere tutti i tipi di problemi apparentemente complessi. Soprattutto quando stai cercando di scrivere una regex complessa per qualcosa come sed, puoi spesso risparmiare ore scrivendola proceduralmente in awk. L'apprendimento comporta grandi dividendi.
Joe

1
@LeviUzodike: Sì awkè un file binario autonomo che deve essere avviato. A differenza di perl o in particolare di Python, l'interprete di awk si avvia rapidamente (ancora tutto il solito sovraccarico di linker dinamico di effettuare parecchie chiamate di sistema, ma awk usa solo libc / libm e libdl. Ad esempio, usa straceper controllare le chiamate di sistema dell'avvio di awk) . Molte shell (come bash) sono piuttosto lente, quindi avviare un processo awk può essere più veloce del loop su token in un elenco con shell incorporate anche per dimensioni di elenco di dimensioni ridotte. E a volte puoi scrivere uno #!/usr/bin/awkscript anziché uno #!/bin/shscript.
Peter Cordes,

35
  • Prima regola di ottimizzazione del software: Non farlo .

    Fino a quando non sai che la velocità del programma è un problema, non è necessario pensare a quanto è veloce. Se la tua lista è di quella lunghezza o è lunga solo ~ 100-1000 articoli, probabilmente non noterai nemmeno quanto tempo impiega. È possibile che passi più tempo a pensare all'ottimizzazione che a quale differenza farebbe.

  • Seconda regola: misura .

    Questo è il modo sicuro per scoprirlo e quello che dà risposte per il tuo sistema. Soprattutto con le conchiglie, ce ne sono così tante e non sono tutte identiche. Una risposta per una shell potrebbe non essere valida per la tua.

    Nei programmi più grandi, la profilazione va anche qui. La parte più lenta potrebbe non essere quella che pensi sia.

  • Terzo, la prima regola di ottimizzazione degli script di shell: non usare la shell .

    Sì davvero. Molte shell non sono fatte per essere veloci (poiché non è necessario l'avvio di programmi esterni) e potrebbero anche analizzare di nuovo le righe del codice sorgente ogni volta.

    Usa invece qualcosa come awk o Perl. In un banale micro-benchmark che ho fatto, awkera dozzine di volte più veloce di qualsiasi shell comune nell'esecuzione di un semplice ciclo (senza I / O).

    Tuttavia, se si utilizza la shell, utilizzare le funzioni predefinite della shell anziché i comandi esterni. Qui stai usando ciò exprche non è incorporato in nessuna shell trovata sul mio sistema, ma che può essere sostituito con un'espansione aritmetica standard. Ad esempio i=$((i+1))invece di i=$(expr $i + 1)incrementare i. L'uso cutdell'ultimo esempio potrebbe anche essere sostituibile con espansioni di parametri standard.

    Vedi anche: Perché usare un loop di shell per elaborare il testo è considerato una cattiva pratica?

I passaggi 1 e 2 dovrebbero applicarsi alla tua domanda.


12
# 0, cita le tue espansioni :-)
Kusalananda

8
Non è che i awkloop siano necessariamente migliori o peggiori dei loop shell. È che la shell è davvero brava nell'eseguire comandi e nel dirigere input e output da e verso i processi, e francamente piuttosto goffa in tutto il resto; mentre strumenti come awksono fantastici nell'elaborazione dei dati di testo, perché è per questo che le shell e gli strumenti come awksono fatti (rispettivamente) in primo luogo.
DopeGhoti,

2
@DopeGhoti, le conchiglie sembrano essere oggettivamente più lente. Alcuni loop mentre sono molto semplici sembrano essere> 25 volte più lenti dashrispetto a con gawk, ed è dashstata la shell più veloce che ho testato ...
ilkkachu

1
@Joe, lo è :) dashe busyboxnon supporta (( .. ))- penso che sia un'estensione non standard. ++è anche esplicitamente menzionato come non richiesto, per quanto ne so, i=$((i+1))o : $(( i += 1))sono quelli sicuri.
ilkkachu,

1
Ri "più tempo a pensare" : questo trascura un fattore importante. Quanto spesso viene eseguito e per quanti utenti? Se un programma perde 1 secondo, che potrebbe essere riparato dal programmatore a pensarci per 30 minuti, potrebbe essere una perdita di tempo se c'è un solo utente che lo eseguirà una volta. D'altra parte se c'è un milione di utenti, questo è un milione di secondi o 11 giorni di tempo dell'utente. Se il codice ha perso un minuto di un milione di utenti, sono circa 2 anni di tempo per l'utente.
agc,

13

Darò solo alcuni consigli generali in questa risposta, e non benchmark. I benchmark sono l'unico modo per rispondere in modo affidabile alle domande sulle prestazioni. Ma dal momento che non dici quanti dati stai manipolando e quanto spesso esegui questa operazione, non c'è modo di fare un benchmark utile. Ciò che è più efficiente per 10 articoli e ciò che è più efficiente per 1000000 articoli spesso non è lo stesso.

Come regola generale, invocare comandi esterni è più costoso che fare qualcosa con costrutti di pura shell, purché il codice di pura shell non implichi un ciclo. D'altra parte, un ciclo di shell che scorre su una stringa di grandi dimensioni o una grande quantità di stringa è probabilmente più lento di una chiamata di uno strumento per scopi speciali. Ad esempio, l'invocazione del tuo ciclo cutpotrebbe essere notevolmente lenta nella pratica, ma se trovi un modo per fare tutto con una singola cutinvocazione, è probabile che sia più veloce che fare la stessa cosa con la manipolazione delle stringhe nella shell.

Si noti che il punto di interruzione può variare molto tra i sistemi. Può dipendere dal kernel, da come è configurato lo scheduler del kernel, dal filesystem contenente gli eseguibili esterni, dalla quantità di CPU rispetto alla pressione di memoria presente al momento e da molti altri fattori.

Non chiamare exprper eseguire l'aritmetica se sei preoccupato per le prestazioni. In effetti, non chiamare exprper eseguire l'aritmetica. Le conchiglie hanno un'aritmetica integrata, che è più chiara e più veloce di invocare expr.

Sembra che tu stia usando bash, dato che stai usando costrutti bash che non esistono in sh. Quindi perché mai non dovresti usare un array? Un array è la soluzione più naturale ed è probabilmente anche la più veloce. Si noti che gli indici di array iniziano da 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

Il tuo script potrebbe essere più veloce se usi sh, se il tuo sistema ha dash o ksh come shanziché bash. Se si utilizza sh, non si ottengono array denominati, ma si ottiene comunque l'array uno dei parametri posizionali, con cui è possibile impostare set. Per accedere a un elemento in una posizione che non è nota fino al runtime, è necessario utilizzare eval(fare attenzione a citare le cose correttamente!).

# 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

Se si desidera accedere all'array solo una volta e si va da sinistra a destra (saltando alcuni valori), è possibile utilizzare shiftinvece di indici variabili.

# 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

L'approccio più rapido dipende dalla shell e dal numero di elementi.

Un'altra possibilità è utilizzare l'elaborazione delle stringhe. Ha il vantaggio di non usare i parametri posizionali, quindi puoi usarli per qualcos'altro. Sarà più lento per grandi quantità di dati, ma è improbabile che faccia una differenza evidente per piccole quantità di dati.

# 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

" D'altra parte, un loop di shell che scorre su una stringa di grandi dimensioni o una grande quantità di stringa è probabilmente più lento di una chiamata di uno strumento per scopi speciali " ma cosa succede se tale strumento ha dei loop in esso come awk? @ikkachu ha detto che i loop awk sono più veloci, ma diresti che con <1000 campi da scorrere, il vantaggio dei loop più veloci non supererebbe il costo di chiamare awk poiché è un comando esterno (supponendo che potrei fare lo stesso compito nella shell loop con l'uso di soli comandi integrati)?
Levi Uzodike,

@LeviUzodike Rileggi il primo paragrafo della mia risposta.
Gilles 'SO- smetti di essere malvagio' il

Potresti anche sostituirlo shift && shift && shiftcon shift 3nel tuo terzo esempio, a meno che la shell che stai usando non lo supporti.
Joe

2
@Joe In realtà, no. shift 3fallirebbe se restassero troppo pochi argomenti. Avresti bisogno di qualcosa di simileif [ $# -gt 3 ]; then shift 3; else set --; fi
Gilles 'SO- smetti di essere malvagio' il

3

awkè un'ottima scelta, se è possibile eseguire tutte le elaborazioni all'interno dello script Awk. Altrimenti, si finisce per reindirizzare l'output di Awk ad altre utility, distruggendo il guadagno di prestazioni di awk.

bashanche l'iterazione su un array è eccezionale, se puoi adattare l'intero elenco all'interno dell'array (che per le shell moderne è probabilmente una garanzia) e non ti dispiace per la ginnastica della sintassi dell'array.

Tuttavia, un approccio alla pipeline:

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

Dove:

  • xargs raggruppa l'elenco separato da spazi in gruppi di tre, ciascuno separato da una nuova riga
  • while read utilizza tale elenco e genera la prima colonna di ciascun gruppo
  • grep filtra la prima colonna (corrispondente a ogni terza posizione nell'elenco originale)

Migliora la comprensibilità, secondo me. Le persone sanno già cosa fanno questi strumenti, quindi è facile da leggere da sinistra a destra e le ragioni di ciò che accadrà. Questo approccio documenta chiaramente anche la lunghezza del passo ( -n3) e il modello di filtro ( 9), quindi è facile variare:

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

Quando poniamo domande di "efficienza", assicurati di pensare a "efficienza totale della vita". Tale calcolo include lo sforzo dei manutentori per mantenere il codice funzionante e noi sacchetti di carne siamo le macchine meno efficienti dell'intera operazione.


2

Forse questo?

cut -d' ' -f1,4,7,10,13 <<<$list
1 5 6 9 15

Mi dispiace non essere stato chiaro prima, ma volevo essere in grado di ottenere i numeri in quelle posizioni senza conoscere la lunghezza della lista. Ma grazie, ho dimenticato che il taglio potrebbe farlo.
Levi Uzodike,

1

Non usare i comandi di shell se vuoi essere efficiente. Limitati a pipe, reindirizzamenti, sostituzioni ecc. E programmi. Ecco perché xargse le parallelutilità esistono - perché bash mentre i loop sono inefficienti e molto lenti. Usa i loop bash solo come ultima risoluzione.

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} 

Ma dovresti probabilmente andare un po 'più veloce con il bene awk.


Mi dispiace non essere stato chiaro prima, ma stavo cercando una soluzione in grado di estrarre i valori solo in base alla loro posizione nell'elenco. Ho appena creato la lista originale in quel modo perché volevo che fossero evidenti i valori che volevo.
Levi Uzodike,

1

Secondo me la soluzione più chiara (e probabilmente anche la più performante) è usare le variabili awk RS e ORS:

awk -v RS=' ' -v ORS=' ' 'NR % 3 == 1' <<< "$list"

1
  1. Usando lo script shell GNU sed e POSIX :

    echo $(printf '%s\n' $list | sed -n '1~3p')
  2. O con bashla sostituzione dei parametri :

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

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

    O più portabilmente, usando sia POSIX sed che shell script:

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

Output di uno di questi:

1 5 6 9 15
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.