为什么echo和cat的执行时间有这么大的差别?

Moh*_*mad 15 shell echo quoting cat command-substitution

回答这个问题让我提出了另一个问题:
我认为下面的脚本做同样的事情,第二个应该快得多,因为第一个使用cat需要一遍又一遍地打开文件但第二个只打开文件一次,然后只回显一个变量:

(有关正确的代码,请参阅更新部分。)

第一的:

#!/bin/sh
for j in seq 10; do
  cat input
done >> output
Run Code Online (Sandbox Code Playgroud)

第二:

#!/bin/sh
i=`cat input`
for j in seq 10; do
  echo $i
done >> output
Run Code Online (Sandbox Code Playgroud)

而输入大约是 50 兆字节。

但是当我尝试第二个时,它太慢了,因为回显变量i是一个巨大的过程。我也遇到了第二个脚本的一些问题,例如输出文件的大小低于预期。

我还检查了手册页echocat比较了它们:

echo - 显示一行文本

cat - 连接文件并在标准输出上打印

但我没有得到区别。

所以:

  • 为什么在第二个脚本中 cat 如此之快而 echo 如此之慢?
  • 还是变量的问题i?(因为在它的手册页中 echo说它显示“一行文本”,所以我猜它只针对短变量进行了优化,而不是像 那样非常长的变量i。然而,这只是一个猜测。)
  • 为什么我在使用时遇到问题echo

更新

我用seq 10而不是`seq 10`错误的。这是编辑后的代码:

第一的:

#!/bin/sh
for j in `seq 10`; do
  cat input
done >> output
Run Code Online (Sandbox Code Playgroud)

第二:

#!/bin/sh
i=`cat input`
for j in `seq 10`; do
  echo $i
done >> output
Run Code Online (Sandbox Code Playgroud)

(特别感谢roaima。)

然而,这不是问题的关键。即使循环只发生一次,我也会遇到同样的问题:catecho.

Sté*_*las 24

这里有几件事情需要考虑。

i=`cat input`
Run Code Online (Sandbox Code Playgroud)

可能很昂贵,而且外壳之间有很多变化。

这是一个称为命令替换的功能。这个想法是将命令的整个输出减去尾随换行符i存储到内存中的变量中。

为此,shell 在子shell 中分叉命令并通过管道或套接字对读取其输出。你在这里看到了很多变化。在此处的 50MiB 文件中,我可以看到例如 bash 比 ksh93 慢 6 倍,但比 zsh 稍快,比yash.

bash缓慢的主要原因是它一次从管道中读取 128 个字节(而其他 shell 一次读取 4KiB 或 8KiB)并且受到系统调用开销的惩罚。

zsh需要做一些后处理来转义 NUL 字节(其他 shell 在 NUL 字节上中断),并yash通过解析多字节字符进行更繁重的处理。

所有 shell 都需要去除尾随的换行符,这可能或多或少地有效。

有些人可能希望比其他人更优雅地处理 NUL 字节并检查它们的存在。

然后,一旦您在内存中拥有那个大变量,对其进行的任何操作通常都涉及分配更多内存和处理数据。

在这里,您将(打算传递)变量的内容传递给echo

幸运的是,echo它内置在您的 shell 中,否则执行可能会因arg list too long错误而失败。即便如此,构建参数列表数组也可能涉及复制变量的内容。

命令替换方法中的另一个主要问题是您正在调用split+glob 运算符(忘记引用变量)。

为此,shell 需要将字符串视为一串字符(尽管有些 shell 没有并且在这方面有问题),因此在 UTF-8 语言环境中,这意味着解析 UTF-8 序列(如果尚未完成yash) ,$IFS在字符串中查找字符。如果$IFS包含空格、制表符或换行符(默认情况下是这种情况),则算法更加复杂和昂贵。然后,需要分配和复制由该拆分产生的单词。

glob 部分将更加昂贵。如果这些单词中的任何一个包含 glob 字符(*, ?, [),那么 shell 将不得不读取某些目录的内容并进行一些昂贵的模式匹配(bash例如, 的实现在这方面非常糟糕)。

如果输入包含类似的内容/*/*/*/../../../*/*/*/../../../*/*/*,那将非常昂贵,因为这意味着列出数千个目录并且可以扩展到数百 MiB。

然后echo通常会做一些额外的处理。一些实现\x在它接收的参数中扩展序列,这意味着解析内容以及可能的另一个数据分配和副本。

另一方面,好的,在大多数 shellcat中不是内置的,这意味着分叉一个进程并执行它(因此加载代码和库),但是在第一次调用之后,该代码和输入文件的内容将缓存在内存中。另一方面,将没有中介。cat将一次读取大量内容并立即写入而无需处理,并且不需要分配大量内存,只需重用一个缓冲区即可。

这也意味着它更可靠,因为它不会阻塞 NUL 字节并且不会修剪尾随的换行符(并且不会执行 split+glob,尽管您可以通过引用变量来避免这种情况,并且不会展开转义序列,尽管您可以通过使用printf代替echo)来避免这种情况。

如果你想进一步优化它,而不是cat多次调用,只需inputcat.

yes input | head -n 100 | xargs cat
Run Code Online (Sandbox Code Playgroud)

将运行 3 个命令而不是 100 个。

为了使变量版本更可靠,您需要使用zsh(其他外壳无法处理 NUL 字节)并执行以下操作:

zmodload zsh/mapfile
var=$mapfile[input]
repeat 10 print -rn -- "$var"
Run Code Online (Sandbox Code Playgroud)

如果您知道输入不包含 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
Run Code Online (Sandbox Code Playgroud)

但这永远不会比cat在循环中使用更有效(除非输入非常小)。


cuo*_*glm 11

问题不在于catand echo,而在于被遗忘的引用变量$i

在类似 Bourne 的 shell 脚本中(除了zsh),不加引号glob+split的变量会导致变量上的运算符。

$var
Run Code Online (Sandbox Code Playgroud)

实际上是:

glob(split($var))
Run Code Online (Sandbox Code Playgroud)

因此,在每次循环迭代中,input(不包括尾随换行符)的全部内容都将被扩展、拆分、通配。整个过程需要shell来分配内存,一次又一次地解析字符串。这就是你表现不佳的原因。

您可以引用该变量来防止,glob+split但它不会对您有多大帮助,因为当 shell 仍然需要构建大字符串参数并扫描其内容时echoecho用外部替换内置/bin/echo将使您的参数列表太长或内存不足取决于$i大小)。大多数echo实现不符合 POSIX,它会\x在收到的参数中扩展反斜杠序列。

使用cat,shell 只需在每次循环迭代中生成一个进程,cat并执行复制 i/o。系统还可以缓存文件内容,使 cat 进程更快。

  • @roaima:你没有提到 glob 部分,这可能是一个很大的原因,对一些`/*/*/*/*../../../../*/*/*/*/* 进行成像/../../../../` 可以在文件内容中。只想指出*细节*。 (2认同)