Bash 参数扩展:速度的最佳实践?

Bash 参数扩展:速度的最佳实践?

我只是想知道是否有人知道任何最佳做法或是否有有关此主题的文档:

场景是在日志文件中搜索/grepping。为了说明我的观点,我将使用ls。假设我运行ls以列出目录中的一系列文件

/var/log/remote/serverX.domain.local/ps/ps2.log.2014-mm-dd.gz

在哪里毫米是月份和日期数字,除了 serverX 之外,还有一整套服务器(例如,我使用 4、5、9、10(这些都是真实服务器)

我跑了ls时间首先使用花括号中的参数列表,然后将其更改为星号,以查看差异。我当然没想到星号的表现会更好。

   emartinez@serverlog:~$ time ls /var/log/remote/server{4,5,9,10}.domain.local/ps/ps2.log.2014-10-0{1,2}.gz
    /var/log/remote/server10.domain.local/ps/ps2.log.2014-10-01.gz  
    ...
    /var/log/remote/server5.domain.local/ps/ps2.log.2014-10-02.gz

real    0m0.004s
user    0m0.010s
sys     0m0.000s

然后我将最后一个花括号替换为星号:

time ls /var/log/remote/server{4,5,9,10}.domain.local/ps/ps2.log.2014-10-0*.gz

我得到了以下统计数据:

    real      0m0.028s
    user      0m0.020s
    sys   0m0.020s

尽管只有 2 个选项,但差别仍然很大,因为可用日期只有 10 月 1 日和 2 日。

我确实再次运行了测试,但这次我将月份替换为列表 {1..12},结果一致:

ps2.log.2014-{1..12}-0{1,2}.gz : real 0m0.010s
ps2.log.2014-{1..12}-0*.gz     : real 0m0.168s

就因为一个星号,差别就这么大!!!速度慢确实有道理,但是有没有基准来衡量到底慢了多少,有没有列出最佳实践?

答案1

例如,似乎应该prefix-*很容易变成,prefix-1 prefix-2因为我们习惯于看到已排序的目录列表。但事实证明,很少有文件系统可以真正生成已排序的文件名列表,而且没有用于请求已排序的文件名列表的标准 API。

如果某个程序(例如ls,或者,就此而言bash)需要文件名列表,则需要读取整个目录列表,该列表将以某种随机顺序生成(通常顺序与创建时间有关;有时它基于文件名的哈希值;但在几乎任何情况下都不是简单的字母顺序)。因此,为了解决 ,prefix-*您需要读取整个目录并根据模式检查每个文件名。由于该过程最昂贵的部分是读取目录,因此模式的复杂程度或与模式匹配的文件名数量无关紧要。

总之,在大型目录中,路径名扩展(“解析 glob”)会很慢。这是避免使用大型目录的原因,而不是避免使用 glob 的原因。

但还有另一个重要的数据点prefix-{1,2}不是路径名扩展。它是“括号扩展“并且它是 Posix shell 标准的扩展(尽管几乎所有 shell 都实现了它)。括号扩展和路径名扩展之间存在许多差异,但一个重要且相关的区别是括号扩展不依赖于文件的存在. 括号扩展是一个简单的字符串操作。

因此,无论这些文件是否存在,它prefix-{1,2}总是会扩展为。这意味着它可以在不读取目录和不加载任何文件的情况下进行扩展。显然,这会很快。但有一个缺点:无法判断结果是否对应于真实文件。prefix-1 prefix-2stat

考虑以下简单示例:

$ mkdir test && cd test
$ touch file1 file2 file4
$ ls file*
file1 file2 file4
$ ls file[1234]
file1 file2 file4
$ ls file{1,2,3,4}
ls: cannot access file3: No such file or directory
file1 file2 file4

最后一点:路径名扩展是由 shell 完成的,而不是ls。使用路径名扩展,我们也可以使用echo

$ echo file*
file1 file2 file4
$ echo file[1234]
file1 file2 file4

并且echo会以更快的速度生成列表,因为echo只需打印其参数,而 while ls(接收相同参数)必须对stat每个参数进行验证,以验证它是否为文件。这stat(这不是一个廉价的调用)在路径名扩展的情况下完全是多余的,因为 shell 已经使用目录列表来过滤文件列表,因此每个传递给的文件名ls都是已知的。(除非 glob 根本不匹配任何文件。)

此外,echo 是bash内置的,因此无需创建子进程即可调用它。

但是,在括号扩展的情况下,echo不会产生相同的结果:

$ echo file{1,2,3,4}
file1 file2 file3 file4

因此我们可以使用ls,将其错误输出重定向到位桶:

$ ls file{1,2,3,4}
file1 file2 file4

在这种情况下,stat调用并不是多余的,因为 shell 从未验证过文件名。

除非你的目录非常大,否则这些都不会有太大区别,而且 glob 会更容易编写。如果你的目录真的很大,您应该考虑将它们分成更小的子目录。

例如,而不是像这样的路径:

/var/log/remote/serverX.domain.local/ps/ps2.log.2014-mm-dd.gz

你可以使用:

/var/log/remote/serverX/domain.local/ps/ps2.log.2014-mm-dd-gz

如果您要永久保留日志,您可能需要提取年份以避免无限增加目录大小:

/var/log/remote/2014/serverX/domain.local/ps/ps2.log.2014-mm-dd-gz

2014故意重复。)

对目录进行分片通常是一个很大的优势,因为它提供了一种优化通配符的机制。如上所述,shell 无法优化

/var/log/remote/server[2357].domain.local/ps/ps2.log.2014-10-*-gz

但它可以优化

/var/log/remote/server[2357]/domain.local/ps/ps2.log.2014-10-*-gz

在第二种情况下,server[2357]只需要与目录名称进行匹配,完成后,ps2.log.2014-10-*-gz只需要与匹配目录中的文件名进行匹配。

答案2

Shell 扩展总是按照特定的顺序进行;括号扩展首先执行,文件名扩展最后执行。

因此,像这样的命令

echo {1..3}*

首先扩展为

echo 1* 2* 3*

1*然后,对、2*和执行文件名扩展3*。每次扩展都涉及遍历目录中的所有文件名并将它们与模式进行比较。

随着目录中单词和/或文件数量的增加,速度会逐渐变慢。即使在空目录中,

shopt -s nullglob  # print nothing for non-matching words
echo {1..1000000}* # prints nothing
shopt -u nullglob  # back to the default

在我的计算机上需要将近五秒钟。考虑到文件名扩展执行了一百万次,这并不奇怪......

一个更快的替代方案是尽可能避免同时使用两种类型的壳扩展

命令

echo [1-1000000]* # also prints nothing

搜索相同的文件名,但使用单一模式。这在我的计算机上花费 33 毫秒。

使用方括号代替花括号还有其他好处:

$ touch 13
$ echo {1..20}*
13 13
$ echo [1..20]*
13

第一种方法找到了文件两次,因为它与模式1*和相匹配13*。这在“纯”文件名扩展中不会发生。

相关内容