Является ли трубопровод, смещение или расширение параметров более эффективным?

26

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

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

Поэтому я хочу иметь возможность просто перебирать список и получать доступ только к 1,5,6,9 и 15.

РЕДАКТИРОВАТЬ: я должен был дать понять, что значения, которые я пытаюсь получить из списка, не должны отличаться по формату от остальной части списка. Что делает их особенными, так это исключительно их позиция в списке (в данном случае это позиция 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 Я почти уверен, что пайпинг делает это худшим вариантом, но я пытался найти метод, который не использует set, из любопытства.

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
Не делая статистики по фактическим случаям вашей проблемы, вы ничего не будете знать. Это включает сравнение с «программированием в awk» и т. Д. Если статистика слишком дорогая, то поиск эффективности, вероятно, не стоит того.
Дэвид Тонхофер
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

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

DopeGhoti
источник
1
Хороший вопрос, @MichaelHomer; Я добавил в сторону вопрос о том, «как определить, какой метод наиболее эффективен ».
DopeGhoti
2
@LeviUzodike Относительно echovs <<<, «идентичный» - слишком сильное слово. Можно сказать, что stuff <<< "$list"это почти идентично printf "%s\n" "$list" | stuff. Что касается echoпротив printf, я направляю вас к этому ответу
JoL
5
@ DopeGhoti На самом деле это так. <<<добавляет новую строку в конце. Это похоже на то, как $()удаляет перевод строки с конца. Это потому, что строки заканчиваются символами новой строки. <<<Выдает выражение в виде строки, поэтому оно должно заканчиваться символом новой строки. "$()"берет строки и предоставляет их в качестве аргумента, поэтому имеет смысл конвертировать, удаляя завершающий символ новой строки.
Йол
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 должны применяться к вашему вопросу.

ilkkachu
источник
12
# 0, процитируйте свои дополнения :-)
Кусалананда
8
Нельзя сказать, что awkпетли обязательно лучше или хуже, чем петли оболочки. Дело в том, что оболочка действительно хороша для запуска команд и для направления ввода и вывода в процессы и из процессов, и, откровенно говоря, довольно неуклюжа во всем остальном; в то время как такие инструменты , как awkэто фантастическое при обработке текстовых данных, потому что это то , что снаряды и инструменты , такие как awkсделано для (соответственно) , в первую очередь.
DopeGhoti
2
@ DopeGhoti, похоже, что снаряды объективно медленнее. Некоторые очень простые циклы while, кажется,> в 25 раз медленнее, dashчем с gawk, и dashбыли самой быстрой оболочкой, которую я тестировал ...
ilkkachu
1
@ Джо, это так :) dashи busyboxне поддерживаю (( .. ))- я думаю, что это нестандартное расширение. ++также явно упоминается как необязательный, насколько я могу судить, i=$((i+1))или : $(( i += 1))являются безопасными.
ilkkachu
1
Re «больше времени на обдумывание» : это пренебрегает важный фактор. Как часто он запускается и для скольких пользователей? Если программа тратит 1 секунду, что может быть исправлено программистом, думающим об этом в течение 30 минут, это может быть пустой тратой времени, если только один пользователь будет запускать ее один раз. С другой стороны, если есть миллион пользователей, это миллион секунд или 11 дней пользовательского времени. Если код потратил минуту на миллион пользователей, это примерно 2 года пользовательского времени.
АРУ
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, если в вашей системе shвместо bash используется dash или ksh . Если вы используете 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
@ Джо На самом деле нет. shift 3потерпит неудачу, если будет слишком мало оставшихся аргументов. Вам нужно что-то вродеif [ $# -gt 3 ]; then shift 3; else set --; fi
Жиль "ТАК - перестань быть злым"
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
doneal24
источник
Извините, я не был понятен раньше, но я хотел, чтобы можно было получить цифры на этих позициях, не зная длины списка. Но спасибо, я забыл, вырезать мог сделать это.
Леви Узодике
1

Не используйте команды оболочки, если вы хотите быть эффективными. Ограничьте себя каналами, перенаправлениями, заменами и т. Д. И программами. Вот почему xargsи parallelутилиты существуют - потому что циклы bash while неэффективны и очень медленны. Используйте петли 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.

KamilCuk
источник
Извините, я не был понятен раньше, но я искал решение, которое могло бы извлечь значения, основываясь только на их позиции в списке. Я только что сделал оригинальный список таким, потому что хотел, чтобы он был очевидным для желаемых значений.
Леви Узодике
1

На мой взгляд, самое ясное решение (и, вероятно, самое эффективное тоже) заключается в использовании переменных awk RS и ORS:

awk -v RS=' ' -v ORS=' ' 'NR % 3 == 1' <<< "$list"
user000001
источник
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. Не- GNU ( то есть POSIX ) sed, и bash:

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

    Или, что более удобно, используя POSIX sed и shell-скрипт:

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

Вывод любого из них:

1 5 6 9 15
АРУ
источник