正在接听这问题让我问了另一个问题:
我认为以下脚本做同样的事情,第二个脚本应该更快,因为第一个脚本cat
需要一遍又一遍地打开文件,但第二个脚本只打开文件一次然后只是回显一个变量:
(有关正确的代码,请参阅更新部分。)
第一的:
#!/bin/sh
for j in seq 10; do
cat input
done >> output
第二:
#!/bin/sh
i=`cat input`
for j in seq 10; do
echo $i
done >> output
而输入约为50兆字节。
但是当我尝试第二个时,它太慢了,因为回显变量i
是一个巨大的过程。我在第二个脚本中也遇到了一些问题,例如输出文件的大小低于预期。
我还检查了手册页echo
并对cat
它们进行了比较:
echo - 显示一行文本
cat - 连接文件并在标准输出上打印
但我没有明白其中的区别。
所以:
- 为什么第二个脚本中 cat 如此快而 echo 如此慢?
- 或者是变量的问题
i
? (因为在它的手册页中echo
据说它显示“一行文字”所以我猜它只针对短变量进行优化,而不是针对像i
.然而,这只是一个猜测。) - 为什么我在使用时遇到问题
echo
?
更新
我用的seq 10
不是`seq 10`
错误的。这是编辑后的代码:
第一的:
#!/bin/sh
for j in `seq 10`; do
cat input
done >> output
第二:
#!/bin/sh
i=`cat input`
for j in `seq 10`; do
echo $i
done >> output
(特别感谢罗埃马.)
然而,这不是问题的重点。即使循环只发生一次,我也会遇到同样的问题:cat
工作速度比echo
.
答案1
这里有几件事需要考虑。
i=`cat input`
可能很昂贵,而且外壳之间有很多差异。
这是一个称为命令替换的功能。这个想法是将命令的整个输出减去尾随换行符存储到i
内存中的变量中。
为此,shell 在子 shell 中分叉命令并通过管道或套接字对读取其输出。你在这里看到很多变化。在此处的 50MiB 文件中,我可以看到 bash 的速度比 ksh93 慢 6 倍,但比 zsh 稍快,是yash
.
缓慢的主要原因bash
是它一次从管道读取 128 个字节(而其他 shell 一次读取 4KiB 或 8KiB),并受到系统调用开销的影响。
zsh
需要进行一些后处理以转义 NUL 字节(其他 shell 在 NUL 字节上中断),并且yash
通过解析多字节字符进行更繁重的处理。
所有 shell 都需要去除尾随的换行符,它们的效率可能或多或少。
有些人可能希望比其他人更优雅地处理 NUL 字节并检查它们是否存在。
然后,一旦内存中有这个大变量,对它的任何操作通常都涉及分配更多内存和跨数据处理。
在这里,您将(打算传递)变量的内容传递给echo
.
幸运的是,echo
它内置在您的 shell 中,否则执行可能会失败并显示参数列表太长错误。即使如此,构建参数列表数组也可能涉及复制变量的内容。
命令替换方法中的另一个主要问题是您正在调用分割+全局 运算符(忘记引用变量)。
为此,shell 需要将字符串视为字符串人物(尽管有些 shell 不这样做,并且在这方面存在缺陷),因此在 UTF-8 语言环境中,这意味着解析 UTF-8 序列(如果尚未像之前那样完成),在字符串中yash
查找字符。$IFS
如果$IFS
包含空格、制表符或换行符(默认情况下就是这种情况),该算法甚至更加复杂和昂贵。然后,需要分配和复制由该分割产生的单词。
glob 部分会更加昂贵。如果这些单词中的任何一个包含全局字符(*
, ?
, [
),那么 shell 将必须读取某些目录的内容并进行一些昂贵的模式匹配(bash
例如, 的实现在这方面非常糟糕)。
如果输入包含类似 的内容/*/*/*/../../../*/*/*/../../../*/*/*
,那将非常昂贵,因为这意味着列出数千个目录,并且可能扩展到数百 MiB。
然后echo
通常会做一些额外的处理。某些实现会扩展\x
它接收的参数中的序列,这意味着解析内容以及可能的数据的另一个分配和副本。
另一方面,好的,在大多数 shell 中cat
不是内置的,因此这意味着分叉一个进程并执行它(因此加载代码和库),但在第一次调用之后,该代码和输入文件的内容将被缓存在内存中。另一方面,不会有中间人。cat
一次会读取大量数据,然后立即写入而不进行处理,并且不需要分配大量内存,只需重用一个缓冲区即可。
这也意味着它更可靠,因为它不会阻塞 NUL 字节,也不会修剪尾随换行符(并且不会执行 split+glob,尽管您可以通过引用变量来避免这种情况,并且不会扩展转义序列,尽管您可以通过使用printf
而不是echo
) 来避免这种情况。
如果你想进一步优化它,不要cat
多次调用,只需传递input
几次到cat
.
yes input | head -n 100 | xargs cat
将运行 3 个命令而不是 100 个。
为了使变量版本更可靠,您需要使用zsh
(其他 shell 无法处理 NUL 字节)并执行以下操作:
zmodload zsh/mapfile
var=$mapfile[input]
repeat 10 print -rn -- "$var"
如果您知道输入不包含 NUL 字节,那么您可以通过 POSIXly 可靠地执行此操作(尽管它可能无法在printf
非内置的地方工作):
i=$(cat input && echo .) || exit # add an extra .\n to avoid trimming newlines
i=${i%.} # remove that trailing dot (the \n was removed by cmdsubst)
n=10
while [ "$n" -gt 10 ]; do
printf %s "$i"
n=$((n - 1))
done
但这永远不会比cat
在循环中使用更有效(除非输入非常小)。
答案2
问题不在于cat
and echo
,而在于被遗忘的 quote 变量$i
。
在类似 Bourne 的 shell 脚本中(除了zsh
),将变量不加引号会导致glob+split
对变量使用运算符。
$var
实际上是:
glob(split($var))
因此,随着每次循环迭代,整个内容input
(不包括尾随换行符)将被扩展、分割、通配。整个过程需要shell分配内存,一次又一次的解析字符串。这就是你表现不佳的原因。
您可以引用该变量来防止glob+split
,但这对您没有多大帮助,因为当 shell 仍然需要构建大字符串参数并扫描其内容时(用外部echo
替换内置将使您的参数列表太长或内存不足)取决于尺寸)。大多数实现不符合 POSIX 标准,它将扩展收到的参数中的反斜杠序列。echo
/bin/echo
$i
echo
\x
使用cat
,shell 只需要在每次循环迭代中生成一个进程,并cat
执行复制 i/o。系统还可以缓存文件内容以使cat处理速度更快。
答案3
如果你打电话
i=`cat input`
这可以让您的 shell 进程增长 50MB 直至 200MB(取决于内部宽字符实现)。这可能会使您的 shell 变慢,但这不是主要问题。
主要问题是上面的命令需要将整个文件读入 shell 内存,并且需要echo $i
对该文件内容进行字段分割$i
。为了进行字段分割,文件中的所有文本都需要转换为宽字符,这是花费大部分时间的地方。
我对慢速情况做了一些测试并得到了这些结果:
- 最快的是 ksh93
- 接下来是我的 Bourne Shell(比 ksh93 慢 2 倍)
- 接下来是 bash(比 ksh93 慢 3 倍)
- 最后是 ksh88(比 ksh93 慢 7 倍)
ksh93 最快的原因似乎是 ksh93 不使用mbtowc()
libc 而是使用自己的实现。
BTW:Stephane 错误地认为读取大小有一定影响,我编译了 Bourne Shell 以读取 4096 字节块而不是 128 字节,并且在两种情况下都获得了相同的性能。
答案4
这echo
意味着在屏幕上放置 1 行。在第二个示例中,您所做的是将文件的内容放入变量中,然后打印该变量。在第一个中,您立即将内容显示在屏幕上。
cat
针对此用途进行了优化。echo
不是。另外,将 50Mb 放入环境变量中也不是一个好主意。