配管,移位或参数扩展更有效吗?


26

我试图找到最有效的方法来迭代某些值,这些值在用空格分隔的单词列表中彼此保持一致的值数(我不想使用数组)。例如,

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

因此,我希望能够仅遍历list并仅访问1,5,6,9和15。

编辑:我应该明确指出,我要从列表中获取的值的格式不必与列表的其余部分不同。使它们与众不同的仅仅是它们在列表中的位置(在这种情况下,位置1,4,7 ...)。因此,列表可能是,1 2 3 5 9 8 6 90 84 9 3 2 15 75 55但我仍然想要相同的数字。而且,假设我不知道列表的长度,我希望能够做到这一点。

到目前为止,我想到的方法是:

方法一

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
如果效率是一个重要的问题,我不会首先使用shell脚本。您的清单有多大作用?
Barmar


2
如果不对问题的实际情况进行统计,您一无所知。这包括将其与“用awk编程”等进行比较。如果统计信息太昂贵,则寻找效率可能不值得。
David Tonhofer

2
李维,您定义中的“有效”方式到底是什么?您想找到一种更快的迭代方法吗?
Sergiy Kolodyazhnyy

Answers:


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,我将您
引向

5
@DopeGhoti确实可以。<<<在末尾添加换行符。这类似于$()从末尾删除换行符的方式。这是因为行由换行符终止。<<<将表达式作为行输入,因此必须以换行符终止。"$()"接受行并将其作为参数,因此通过删除终止的换行符进行转换是有意义的。
JoL

3
@LeviUzodike awk是一个被低估的工具。这将使各种看似复杂的问题易于解决。尤其是当您尝试为sed之类的东西编写复杂的正则表达式时,通常可以通过以awk的方式编写它来节省时间。学习它会带来很多好处。

1
@LeviUzodike:是的awk,必须启动一个独立的二进制文件。与perl或特别是Python不同,awk解释器快速启动(仍然需要进行许多系统调用的所有常见动态链接器开销,但是awk仅使用libc / libm和libdl。例如,用于strace签出awk启动的系统调用) 。许多shell(例如bash)的运行速度都很慢,因此启动awk进程比循环使用内置shell的列表中的令牌要快,即使对于较小的列表也是如此。有时候,你可以写一个#!/usr/bin/awk脚本,而不是一个#!/bin/sh脚本。
Peter Cordes

35
  • 软件优化的首要规则:请勿

    除非您知道程序的速度是一个问题,否则无需考虑它的速度如何。如果您的清单大约是这个长度,或者只是大约100-1000个项目,那么您甚至可能根本不会注意到它要花多长时间。您有更多的时间在考虑优化问题,而不是有什么区别。

  • 第二条规则:测量

    这是找出答案的可靠方法,并且可以为您的系统提供答案。尤其是对于贝壳而言,有很多东西,而且它们并不完全相同。一个壳的答案可能不适用于您的壳。

    在较大的程序中,性能分析也在这里进行。最慢的部分可能不是您认为的那部分。

  • 第三,shell脚本优化的第一条规则:不要使用shell

    是的,真的。许多shell并不是很快(因为不必启动外部程序),它们甚至可能每次都再次解析源代码行。

    请改用awk或Perl之类的东西。在一个微不足道的微基准测试中,awk运行一个简单的循环(没有I / O)的速度比任何普通Shell快数十倍。

    但是,如果确实使用外壳程序,请使用外壳程序的内置函数而不是外部命令。在这里,您正在使用expr的不是我在系统上找到的任何Shell中内置的,而是可以用标准算术扩展替换的。例如,i=$((i+1))而不是i=$(expr $i + 1)增加i。您cut在最后一个示例中的使用也可以用标准参数扩展替换。

    另请参阅:为什么使用shell循环处理文本被认为是不良做法?

步骤#1和#2应该适用于您的问题。


12
#0,引用您的扩展:-)
库萨兰达

8
并不是说awk循环一定比shell循环更好或更糟。Shell确实非常擅长运行命令以及在进程之间进行输入和输出定向,坦率地说,在其他所有方面都很笨拙。诸如此类awk的工具在处理文本数据方面非常出色,因为首先是针对此类外壳和工具awk进行的。
DopeGhoti

2
@DopeGhoti,尽管如此,shell确实确实要慢一些。一些非常简单的while循环似乎dash比使用慢25倍gawk,并且dash是我测试过的最快的shell ...
ilkkachu

1
@Joe是:) dash并且busybox不支持(( .. ))-我认为这是一个非标准扩展。据我所知,++明确提到不是必需的,i=$((i+1))还是: $(( i += 1))安全的。
ilkkachu

1
关于“更多时间的思考”:这是一个重要因素。它多久运行一次,有多少用户?如果程序浪费了1秒钟(可以由程序员考虑30分钟来修复),那么如果只有一个用户要运行一次,则可能会浪费时间。另一方面,如果有100万用户,则是一百万秒或11天的用户时间。如果代码浪费了一百万用户一分钟,那大约是两年的用户时间。
agc

13

我只会在此答案中提供一些一般性建议,而不是基准测试。基准测试是可靠地回答有关性能问题的唯一方法。但是,由于您没有说明要处理多少数据以及执行此操作的频率,因此无法进行有用的基准测试。10个项目的效率更高,而1000000个项目的效率更高通常是不同的。

作为一般经验法则,只要纯shell代码不涉及循环,调用外部命令比使用纯shell构造进行操作要昂贵。另一方面,迭代大型字符串或大量字符串的shell循环可能比专用工具的一次调用慢。例如,循环调用cut在实践中很可能会明显变慢,但是,如果您找到了一种通过一次cut调用来完成整个事情的方法,那可能比在shell中使用字符串操作来完成同一件事更快。

请注意,系统之间的截止点可能会有很大的不同。它可能取决于内核,内核的调度程序的配置方式,包含外部可执行文件的文件系统,当前有多少CPU压力与内存压力以及许多其他因素。

expr如果您完全关心性能,请不要打电话进行算术运算。实际上,根本不要调用expr执行算术。Shell具有内置的算法,它比调用更清晰,更快捷expr

您似乎正在使用bash,因为您正在使用sh中不存在的bash构造。那么为什么地球上不使用数组呢?数组是最自然的解决方案,而且可能也是最快的解决方案。请注意,数组索引从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

另一方面,在大型字符串或大量字符串上进行迭代的Shell循环可能比对专用工具的调用要慢 ”,但如果该工具在其中像awk那样循环,该怎么办?@ikkachu说awk循环更快,但是您想说,通过<1000个字段进行迭代,更快的循环的好处不会超过调用awk的代价,因为它是一个外部命令(假设我可以在shell中执行相同的任务使用仅内置命令循环)?
Levi Uzodike

@LeviUzodike请重新阅读我的答案的第一段。
吉尔(Gilles)“所以,别再邪恶了”

您也可以在第三个示例shift && shift && shift中用替换为shift 3-除非您使用的shell不支持它。

2
@乔其实不行。shift 3如果剩余参数太少,则将失败。您需要类似if [ $# -gt 3 ]; then shift 3; else set --; fi
Gilles

3

awk如果您可以在Awk脚本中进行所有处理,是一个不错的选择。否则,您最终只能将Awk输出传送到其他实用程序,从而破坏的性能awk

bash如果您可以将整个列表放入数组中(对于现代shell来说可能是一个保证),并且不介意数组语法,那么在数组上进行迭代也很棒。

但是,管道方法:

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

抱歉,我之前不清楚,但我希望能够在不知道列表长度的情况下获得这些位置的数字。但是,谢谢,我忘了切可以做到。
Levi Uzodike

1

如果您想提高效率,请不要使用shell命令。将自己限制为管道,重定向,替换等和程序。这就是为什么xargsparallel公用事业存在的-因为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


抱歉,我之前不清楚,但是我在寻找一种解决方案,该解决方案只能根据值在列表中的位置提取值。我只是像这样制作原始列表,因为我希望它可以很明显地体现出我想要的值。
Levi Uzodike

1

在我看来,最清晰的解决方案(也许也是性能最高的解决方案)是使用RS和ORS awk变量:

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

1
  1. 使用GNU sedPOSIX Shell脚本:

    echo $(printf '%s\n' $list | sed -n '1~3p')
  2. 或使用bash参数替换

    echo $(sed -n '1~3p' <<< ${list// /$'\n'})
  3. GNU POSIXsed,以及bash

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

    或更可移植的是,同时使用POSIX sed和Shell脚本:

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

以下任何一项的输出:

1 5 6 9 15
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.