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

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

正在接听问题让我问了另一个问题:
我认为以下脚本做同样的事情,第二个脚本应该更快,因为第一个脚本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

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

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

$var

实际上是:

glob(split($var))

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

您可以引用该变量来防止glob+split,但这对您没有多大帮助,因为当 shell 仍然需要构建大字符串参数并扫描其内容时(用外部echo替换内置将使您的参数列表太长或内存不足)取决于尺寸)。大多数实现不符合 POSIX 标准,它将扩展收到的参数中的反斜杠序列。echo/bin/echo$iecho\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 放入环境变量中也不是一个好主意。

相关内容