Что является более эффективным: использование каналов, сдвига или расширения параметров?

Вопрос или проблема

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

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

Итак, что будет наиболее эффективно, или, может быть, я упускаю более простой метод?

  • Первое правило оптимизации программного обеспечения: Не делайте этого.

    Пока вы не знаете, что скорость выполнения программы является проблемой, нет необходимости думать
    о том, насколько быстро она выполняется. Если ваш список имеет такую длину или около 100-1000 пунктов,
    вы, вероятно, даже не заметите, сколько времени это занимает. Возможно, вы тратите больше времени на размышления об оптимизации, чем сколько составит разница.

  • Второе правило: Измеряйте.

    Это единственный надежный способ выяснить это, и единственный, который дает ответы для вашей системы.
    Особенно с шеллами, их так много, и они не все одинаковы. Ответ для одного шелла может не подходить для вашего.

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

  • Третье, первое правило оптимизации шелл-скрипта: Не используйте шелл.

    Да, серьезно. Многие шеллы не предназначены для быстроты (потому что запуск внешних программ не обязательно должен быть быстрым), и они могут даже повторно разбирать строки исходного кода каждый раз.

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

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

    См. также: Почему использование шелл-цикла для обработки текста считается плохой практикой?

Шаги №1 и №2 должны применяться к вашему вопросу.

Довольно просто с 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

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

Я собираюсь предоставить несколько общих советов в этом ответе, а не бенчмарков. Бенчмарки — это единственный способ надежно отвечать на вопросы о производительности. Но поскольку вы не указали, сколько данных вы обрабатываете и как часто выполняете эту операцию, невозможно выполнить полезный бенчмарк. То, что более эффективно для 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, если ваша система имеет dash или ksh в качестве sh, а не bash. Если вы используете sh, у вас нет именованных массивов, но у вас все еще есть массив одного из позиционных параметров, который вы можете установить с помощью set. Чтобы получить доступ к элементу на позиции, которая неизвестна до выполнения, вам нужно использовать eval (будьте осторожны с правильным заключением в кавычки!).

# Элементы списка не должны содержать пробелы или ?*[
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="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="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 — отличный выбор, если вы можете выполнить всю свою обработку внутри 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"

Когда мы задаём вопросы об “эффективности”, обязательно думайте о “общей жизненной эффективности”. Этот расчет включает в себя усилия по поддержке, которые требуются для поддержания работы кода, и мы, люди, являемся наименее эффективными машинами в этой цели.

Возможно, это?

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

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

На мой взгляд, самое простое решение (и, вероятно, наиболее производительное) – это использовать переменные RS и ORS в awk:

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

  1. С использованием GNU sed и POSIX шелл-скрипта:

    echo $(printf '%s\n' $list | sed -n '1~3p')
    
  2. Или с bash с использованием замены параметров:

    echo $(sed -n '1~3p' <<< ${list// /$'\n'})
    
  3. Не-GNU (то есть POSIX) sed и bash:

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

    Или, более портативно, с использованием POSIX sed и шелл-скрипта:

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

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

1 5 6 9 15

Как уже упоминали другие, существует множество способов решения проблемы, но термин “эффективность” зависит от конкретного сценария использования.

Массивы, кстати, очень эффективны, особенно когда используется форма присваивания массива и строки, разделённые новыми строками (также содержащие пробелы и специальные символы), могут быть очень быстро преобразованы в массив, используя ограниченный IFS:

var="element 1"$'\n'"element 2"$'\n'"element 3"
# Ограничение IFS только новыми строками
_IFS="$IFS"; IFS=$'\n'
array=($var)
# Восстановление значения IFS по умолчанию
IFS="$_IFS"

В частности, использовать их для большого количества элементов намного эффективнее, чем строить объединённую строку – см. мой анализ производительности различных вариантов фильтрации для различных наборов и итераций над элементами здесь.

Сохранение результатов в массиве также гораздо удобнее для дальнейшей обработки.

Вот мои 5 центов к первоначальной проблеме, как я её понимаю – с учетом того, что ваш список не содержит строк с пробелами (что вызвало бы их разделение, итерируя по частям как по аргументам списка).

Вы уже были близки – если вы хотите только печатать или собирать каждый 3-й элемент; поскольку Bash не имеет do-while, просто выведите/сохраните первый аргумент сразу, перед тем, как сдвигать их дальше в списке:

set -- $list
# Сохраните каждый первый аргумент, если он не пуст
[[ $1 ]] && result=("$1")
# Сохраните каждый третий аргумент, если он не пуст
while shift 3; do
    [[ $1 ]] && result+=("$1")
done
# Напечатать результаты, если найдены
[[ ${#result[@]} -gt 0 ]] && printf '%s\n' 'НАЙДЕНО:' && printf '%s\n' "${result[@]}"

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

Вариант:

set -- $list
[[ $1 ]] && result=("$1")
while shift; do
# Сохраните каждый третий аргумент, если он не пуст
if [[ $((++i % 3)) -eq 0 ]]; then
    [[ $1 ]] && result+=("$1")
fi
[[ ${#result[@]} -gt 0 ]] && printf '%s\n' 'НАЙДЕНО:' && printf '%s\n' "${result[@]}"

Или, разделив оригинальную строку в массив, в котором могут быть легко применены фильтры:

# Ограничение IFS на пробел, в соответствии с проблемой выше
_IFS="$IFS"; IFS=' '
array=($line)
IFS="$_IFS"
for e in "${array[@]}"; do 
    [[ $e == [0-9] ]] && result+=("$e")    
done
[[ ${#result[@]} -gt 0 ]] && printf '%s\n' 'НАЙДЕНО:' && printf '%s\n' "${result[@]}"

Это то, что я предпочёл бы, это также должно хорошо масштабироваться.

.

Ответ или решение

Вопрос об эффективности использования различных методов обработки строк в командной оболочке является важным, особенно в контексте автоматизации задач и обработки данных в ИТ-инфраструктуре. Когда заходит речь о таких понятиях, как пайпинг (piping), сдвиг (shifting) или расширение параметров (parameter expansion), следует учитывать несколько ключевых аспектов, которые я опишу согласно методологии TEA (Теория, Пример, Применение).

Теория

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

  1. Объём данных: Большие наборы данных требуют более оптимизированных подходов из-за времени, затрачиваемого на выполнение скрипта.
  2. Ресурсы системы: Использование процессора и оперативной памяти может значительно различаться в зависимости от выбранного подхода.
  3. Вызовы внешних команд: Каждое обращение к внешним программам в Unix/Linux окружении требует создания нового процесса, что увеличивает время выполнения.

Пример

Рассмотрим классический пример обработки списка:

list="1 2 3 5 9 8 6 90 84 9 3 2 15 75 55"

Ваша цель состоит в извлечении определённых элементов на основании их позиции в списке (например, элементы 1, 4, 7 и так далее). Для этого мы можем использовать различные методы:

  1. Метод с использованием цикла и команды shift:

    • Этот метод работает за счёт последовательного "сдвига" элементов списка, что делает его более наглядным, но может быть менее эффективным из-за потенциально большого количества операций с переменными.
  2. Использование awk:

    • Команда awk позволяет эффективно обрабатывать данные, поскольку она разработана специально для текстовых манипуляций и позволяет обойтись без создания множества процессов. Например:
      awk -F' ' '{for( i=1; i<=NF; i+=3) { print $i } }' <<< "$list"
  3. Пайпинг с использованием cut:

    • Метод, который включает в себя использование команд пайпинга, таких как cut, более нагляден, но потенциально может быть менее эффективным из-за необходимости многократного вызова внешних команд.

Применение

В реальной практике выбор метода обработки данных должен основываться не только на теоретической эффективности, но и на специфике вашей задачи, системе, на которой выполняется скрипт, и объёме данных. Например, если у вас сотни тысяч записей, то использование awk станет наиболее оптимальным решением благодаря его способности эффективно работать с большими текстовыми файлами. Однако для небольших скриптов, выполняемых на разовых задачах, можно использовать более простой подход с shift, особенно если читабельность кода важнее его производительности.

Помимо этого, рассмотрите возможность использования встроенных возможностей языка Bash, таких как массивы, которые могут значительно упростить логику обработки с сопоставимой производительностью:

IFS=' ' read -r -a array <<< "$list"
for ((i=0; i<${#array[@]}; i+=3)); do
    echo "${array[i]}"
done

Этот подход удобен, если ваши данные не требуют сложной манипуляции с текстом, но позволяет четко работать с позициями и индексами, обеспечивая ясность кода и его потенциал для последующего расширения.

Итак, выбор метода зависит от специфики вашей задачи и системы, на которой происходит выполнение. Оценка производительности может быть выполнена с использованием анализа времени выполнения команд, например, с помощью утилиты time, что позволит вам принять более информированное решение.

Этот теоретический и практический подход к проблеме поможет вам выбрать наиболее подходящую стратегию для ваших задач в ИТ-инфраструктуре, обеспечивая баланс между производительностью и сложностью кода.

Оцените материал
Добавить комментарий

Капча загружается...