Чи ефективніше розширення трубопроводів, зміщення чи розширення параметрів?


26

Я намагаюся знайти найефективніший спосіб ітерації за допомогою певних значень, які є послідовною кількістю значень подалі одне від одного у списку розділених пробілами слів (я не хочу використовувати масив). Наприклад,

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

Тому я хочу мати змогу просто повторити список і отримати доступ лише до 1,5,6,9 та 15.

EDIT: Я мав би дати зрозуміти, що значення, які я намагаюся отримати зі списку, не повинні відрізнятися за форматом від решти списку. Те, що робить їх особливими, полягає лише у їхній позиції у списку (у цьому випадку позиція 1,4,7 ...). Тож список міг би бути,1 2 3 5 9 8 6 90 84 9 3 2 15 75 55але я все одно хочу однакові номери. А також я хочу це зробити, припускаючи, що не знаю тривалості списку.

Дотепер я думав про методи:

Спосіб 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

Спосіб 2

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

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

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

То що було б найефективнішим, чи я пропускаю більш простий метод?


10
Я б не використовував сценарій оболонки в першу чергу, якщо ефективність є важливою проблемою. Наскільки великий ваш список, який він змінює?
Вармар


2
Не роблячи статистики щодо фактичних випадків вашої проблеми, ви нічого не знаєте. Це включає порівняння з "програмуванням у див" тощо. Якщо статистика занадто дорога, шукати ефективність, мабуть, не варто.
Девід Тонхофер

2
Леві, що саме є "ефективним" способом у твоєму визначенні? Ви хочете знайти більш швидкий спосіб ітерації?
Сергій Колодяжний

Відповіді:


18

Досить простий з awk. Ви отримаєте значення кожного четвертого поля для введення будь-якої довжини:

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

Ця робота полягає у використанні вбудованих awkзмінних, таких як NF(кількість полів у записі), та простого forциклічного повторення по полях, щоб отримати ті, які ви хочете, не знаючи заздалегідь, скільки їх буде.

Або, якщо ви дійсно просто хочете тих специфічних полів, як зазначено у вашому прикладі:

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

Щодо питання про ефективність, найпростішим шляхом було б перевірити цей чи кожен із інших ваших методів і використовувати, timeщоб показати, скільки часу потрібно; Ви також можете використовувати такі інструменти, як straceподивитися, як протікають системні дзвінки. Використання timeвигляду:

$ time ./script.sh

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

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


1
Хороший момент, @MichaelHomer; Я додав убік, вирішуючи питання "як я можу визначити, який метод є найбільш ефективним ".
DopeGhoti

2
@LeviUzodike Стосовно echovs <<<"ідентичний" занадто сильне слово. Можна сказати, stuff <<< "$list"це майже ідентично printf "%s\n" "$list" | stuff. Щодо echovs printf, то я спрямовую вас на цю відповідь
JoL

5
@DopeGhoti Насправді це так. <<<додає новий рядок наприкінці. Це схоже на те, як $()видаляє новий рядок з кінця. Це тому, що рядки закінчуються новими рядками. <<<подає вираз як рядок, тому він повинен бути закінчений новим рядком. "$()"приймає рядки та надає їх як аргумент, тому має сенс перетворити, видаливши закінчуючий новий рядок.
JoL

3
@LeviUzodike awk - це дуже недооцінений інструмент. Це зробить всілякі, здавалося б, складні проблеми легко вирішити. Особливо, коли ви намагаєтесь написати складний регулярний вираз для чогось подібного sed, ви можете часто економити години, замість того, щоб записати це процедурно в awk. Вивчення цього призведе до великих дивідендів.
Джо

1
@LeviUzodike: Так awk- це окремий бінарний файл, який потрібно запустити. На відміну від perl або особливо Python, інтерпретатор awk запускається швидко (все-таки всі звичайні динамічні накладні зв’язки здійснюють досить багато системних викликів, але awk використовує лише libc / libm та libdl. Наприклад, використовувати straceдля перевірки системних викликів запуску awk) . Багато оболонок (як bash) досить повільні, тому запускати один процес awk може бути швидше, ніж перебирати маркери в списку з вбудованими оболонками навіть для невеликих розмірів списку. А іноді ви можете написати #!/usr/bin/awkскрипт , а не у вигляді #!/bin/shсценарію.
Пітер Кордес

35
  • Перше правило оптимізації програмного забезпечення: Не варто .

    Поки ви не знаєте, що швидкість програми є проблемою, не потрібно думати про те, наскільки вона швидка. Якщо у вашому списку приблизно така довжина або просто ~ 100-1000 елементів, ви, ймовірно, навіть не помітите, скільки часу це займе. Є ймовірність, що ви витратите більше часу на роздуми про оптимізацію, ніж те, якою буде різниця.

  • Друге правило: міра .

    Це вірний спосіб дізнатися і той, який дає відповіді для вашої системи. Особливо з оболонками їх дуже багато, і вони не всі однакові. Відповідь на одну оболонку може не стосуватися вашої.

    У більш великих програмах тут також йде профілювання. Найповільніша частина може бути не такою, яку ви думаєте.

  • По-третє, перше правило оптимізації сценарію оболонки: не використовуйте оболонку .

    Так, справді. Багато оболонок зроблені не так швидко (оскільки не потрібно запускати зовнішні програми), і вони можуть навіть кожен раз знову розбирати рядки вихідного коду.

    Використовуйте щось на зразок awk або Perl. У тривіальному мікро-бенчмарку, який я зробив, awkбув у десятки разів швидшим, ніж будь-яка звичайна оболонка у виконанні простого циклу (без вводу / виводу).

    Однак якщо ви використовуєте оболонку, використовуйте вбудовані функції оболонки замість зовнішніх команд. Тут ви використовуєте те, exprщо не вбудовано в жодних оболонок, які я знайшов у своїй системі, але які можна замінити стандартними арифметичними розширеннями. Наприклад, i=$((i+1))замість i=$(expr $i + 1)приросту i. Ваше використання cutв останньому прикладі також може бути замінено стандартними розширеннями параметрів.

    Дивіться також: Чому використання циклу оболонки для обробки тексту вважається поганою практикою?

Кроки №1 та №2 повинні стосуватися вашого питання.


12
# 0,
цитуйте

8
Справа не в тому, що awkпетлі обов'язково є кращими або гіршими, ніж петлі оболонки. Це те, що оболонка справді гарна у виконанні команд, в керуванні входом і виходом до процесів і з них, і, відверто кажучи, досить чіткою щодо всього іншого; в той час як awkтакі інструменти як фантастичні при обробці текстових даних, тому що саме такі оболонки та інструменти, як наприклад awk, створені (відповідно) для (відповідно).
DopeGhoti

2
@DopeGhoti, проте оболонки здаються об'єктивно повільнішими, хоча. Деякі дуже прості петлі здаються> в 25 разів повільнішими, dashніж з gawk, і це dashбула найшвидша оболонка, яку я протестував ...
ilkkachu

1
@Joe, так це :) dashі busyboxне підтримую (( .. ))- я думаю, що це нестандартне розширення. ++також явно згадується як не потрібно, наскільки я можу сказати, i=$((i+1))чи : $(( i += 1))це безпечні.
ilkkachu

1
Повторно "більше часу на мислення" : це нехтує важливим фактором. Як часто він працює і для скільки користувачів? Якщо програма втрачає 1 секунду, яку програміст може зафіксувати, думаючи про це протягом 30 хвилин, це може бути марною витратою часу, якщо один раз запустить її. З іншого боку, якщо є мільйон користувачів, це мільйон секунд або 11 днів користувальницького часу. Якщо код витратив хвилину мільйона користувачів, це приблизно 2 роки користувальницького часу.
agc

13

У цій відповіді я лише дам деякі загальні поради, а не орієнтири. Тести - єдиний спосіб надійно відповісти на запитання щодо продуктивності. Але оскільки ви не кажете, скільки даних ви маніпулюєте, і як часто ви виконуєте цю операцію, немає ніякого способу зробити корисний орієнтир. Що ефективніше для 10 предметів, а що ефективніше для 1000000 предметів, це часто не те саме.

Як правило, виклик зовнішніх команд коштує дорожче, ніж робити щось із чистими конструкціями оболонок, якщо чистий код оболонки не передбачає циклу. З іншого боку, цикл оболонки, який повторюється над великою або великою кількістю рядка, ймовірно, буде повільнішим, ніж одне виклик інструменту спеціального призначення. Наприклад, ваш цикл виклику cutможе бути помітно повільним на практиці, але якщо ви знайдете спосіб зробити всю справу за допомогою одного cutвиклику, що, швидше за все, буде швидше, ніж зробити те ж саме за допомогою маніпуляції з рядком в оболонці.

Зауважте, що точка відсікання може сильно відрізнятися між системами. Це може залежати від ядра, від того, як налаштовано планувальник ядра, файлової системи, що містить зовнішні виконувані файли, від того, який тиск процесора та пам'яті існує на даний момент, та багатьох інших факторів.

Не закликайте exprвиконувати арифметику, якщо ви взагалі стурбовані продуктивністю. Насправді взагалі не закликайте exprвиконувати арифметику. Оболонки мають вбудовану арифметику, яка зрозуміліша і швидша, ніж викликати expr.

Здається, ви використовуєте bash, оскільки ви використовуєте bash конструкції, яких немає в sh. То чому б на землі ви не використовували масив? Масив - це найбільш природне рішення, і це, швидше за все, буде також найшвидшим. Зауважте, що індекси масиву починаються з 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

Ваш сценарій може бути швидшим, якщо ви використовуєте sh, якщо ваша система має тире або ksh, shа не bash. Якщо ви використовуєте sh, ви не отримаєте назви масивів, але ви все одно отримаєте масив одного з позиційних параметрів, з яким ви можете встановити set. Для доступу до елемента в позиції, яка не відома до часу виконання, вам потрібно скористатися eval(подбайте про те, щоб цитувати речі правильно!).

# 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

Якщо ви хочете лише один раз отримати доступ до масиву і збираєтесь зліва направо (пропускаючи деякі значення), ви можете використовувати shiftзамість змінних індексів.

# 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

Який підхід швидший, залежить від оболонки та кількості елементів.

Інша можливість - використовувати обробку рядків. Має перевагу не використовувати позиційні параметри, тому ви можете використовувати їх для чогось іншого. Це буде повільніше для великого обсягу даних, але це навряд чи помітно зміниться для невеликих обсягів даних.

# 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

" З іншого боку, цикл оболонки, який повторюється над великим рядком або великою кількістю рядка, ймовірно, буде повільнішим, ніж одне виклик інструменту спеціального призначення ", але що робити, якщо цей інструмент має петлі в ньому, як awk? @ikkachu сказав, що цикли awk - це швидше, але ви б сказали, що з <1000 полів для повторення, користь більш швидких циклів не перевищує вартість виклику awk, оскільки це зовнішня команда (припустимо, що я можу виконати те саме завдання в оболонці циклів із застосуванням лише вбудованих команд)?
Леві Узодике

@LeviUzodike Будь ласка, перечитайте перший параграф моєї відповіді.
Жил "ТАК - перестань бути злим"

Можна також замінити shift && shift && shiftз shift 3вашим третім прикладом - якщо оболонка ви використовуєте не підтримує його.
Джо

2
@Joe Власне, ні. shift 3не вдалося б, якби залишилось занадто мало аргументів. Вам знадобиться щось на кшталтif [ $# -gt 3 ]; then shift 3; else set --; fi
Жил "SO- перестань бути злим"

3

awk- чудовий вибір, якщо ви можете виконати всю обробку всередині сценарію Awk. В іншому випадку ви просто в кінцевому підсумку передаєте вихід Awk іншим утилітам, знищуючи підвищення продуктивності awk.

bashітерація над масивом також чудова, якщо ви можете вмістити весь список усередині масиву (що, можливо, для сучасних оболонок, мабуть, гарантія), і ви не заперечуєте проти синтаксичної гімнастики масиву.

Однак трубопровідний підхід:

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

Де:

  • xargs групує розділений пробілом список на групи з трьох, кожен новий рядок розділений
  • while read споживає цей список і виводить перший стовпець кожної групи
  • grep фільтрує перший стовпець (відповідає кожній третій позиції у вихідному списку)

Поліпшує зрозумілість, на мій погляд. Люди вже знають, що роблять ці інструменти, тому легко читати зліва направо та міркувати про те, що буде. Цей підхід також чітко документує довжину кроку ( -n3) та шаблон фільтра ( 9), тому легко змінювати:

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

Коли ми задаємо питання "ефективність", обов'язково подумайте про "загальну ефективність життя". Цей підрахунок включає зусилля обслуговуючого персоналу, щоб код не працював, і ми, м'ясні пакетики, є найменш ефективними машинами за всю операцію.


2

Можливо, це?

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

Вибачте, мені раніше не було зрозуміло, але я хотів отримати можливість отримати номери на цих посадах, не знаючи тривалості списку. Але дякую, я забув, що вирізати це могло.
Леві Узодике

1

Не використовуйте команди оболонки, якщо хочете бути ефективними. Обмежтеся лише трубами, перенаправленнями, замінами тощо та програмами. Ось чому xargsі parallelутиліти існують - адже башти, поки петлі неефективні і дуже повільні. Використовуйте bash петлі лише як останню розв’язку.

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} 

Але вам слід отримати швидше трохи швидше з добром awk.


Вибачте, я раніше не був зрозумілим, але шукав рішення, яке могло б витягнути значення, виходячи лише з їхньої позиції в списку. Я просто склав такий оригінальний список, тому що хотів, щоб це були очевидні значення, які я хотів.
Леві Узодике

1

На мою думку, найяскравішим рішенням (і, мабуть, і найефективнішим) є використання змінних RS і ORS awk:

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

1
  1. Використання сценарію оболонки GNU sed та POSIX :

    echo $(printf '%s\n' $list | sed -n '1~3p')
  2. Або з заміною параметрівbash 's :

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

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

    Або ще більш портативно, використовуючи і POSIX, sed і сценарій оболонки:

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

Вихід будь-якого з них:

1 5 6 9 15
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.