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


15

回答这个问题使我又问了一个问题:
我认为以下脚本可以做同样的事情,第二个脚本应该更快,因为第一个脚本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是一个巨大的过程。第二个脚本也有一些问题,例如输出文件的大小比预期的要小。

我还检查了的手册页echocat进行比较:

回声-显示一行文本

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

(特别感谢roaima。)

但是,这不是问题的重点。即使循环只发生一次,我也会遇到相同的问题:cat比的运行速度快得多echo


1
那又如何cat $(for i in $(seq 1 10); do echo "input"; done) >> output呢?:)
netmonk

2
echo速度更快。您所缺少的是,通过使用变量时不引用变量,使shell做太多的工作。
roaima

引用变量不是问题。问题出在变量i本身(即,将其用作输入和输出之间的中间步骤)。
Aleksander 2015年

`echo $ i`-不要这样做。使用printf并引用参数。
PSkocik

1
@PSkocik我的意思是你想要的printf '%s' "$i",不是echo $i。@cuonglm在回答中很好地解释了回声的一些问题。对于为什么即使报价是在某些情况下,与之呼应不够,看unix.stackexchange.com/questions/65803/...
PSkocik

Answers:


24

这里有几件事情要考虑。

i=`cat input`

可能会很昂贵,而且外壳之间会有很多差异。

这就是所谓的命令替换功能。想法是将命令的整个输出减去结尾的换行符i存储到内存中的变量中。

为此,外壳程序将命令分叉到子外壳程序中,并通过管道或套接字对读取其输出。您会在这里看到很多变化。在这里的50MiB文件中,我可以看到bash的速度是ksh93的6倍,但比zsh稍快,是bsh的两倍yash

bash速度慢的主要原因是它一次从管道读取128个字节(而其他Shell一次读取4KiB或8KiB),并且受到系统调用开销的不利影响。

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

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

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

然后,一旦您在内存中拥有了一个大变量,对它的任何操作通常都会涉及分配更多的内存并应对数据。

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

幸运的是,它echo是内置在您的Shell中的,否则执行可能会因arg列表太长而失败。即使这样,构建参数列表数组也可能会涉及复制变量的内容。

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

为此,壳需要把字符串作为一个字符串的字符(虽然有些炮弹不和是在这方面的电瓶车)所以在UTF-8语言环境,这意味着解析UTF-8序列(如果不这样做已经喜欢yash做) ,$IFS在字符串中查找字符。如果$IFS包含空格,制表符或换行符(默认情况下是这种情况),该算法将更加复杂且昂贵。然后,需要分配和复制该拆分产生的单词。

球形部分将更加昂贵。如果有任何的这些话包含水珠字符(*?[),然后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在循环中使用更有效(除非输入很小)。


值得一提的是,在长争论的情况下,您可能会失去记忆。示例/bin/echo $(perl -e 'print "A"x999999')
cuonglm

您可能会误以为阅读大小会产生重大影响,因此请阅读我的答案以了解真正的原因。
schily

@schily,执行128字节的409600次读取比800k的64k读取花费更多的时间(系统时间)。dd bs=128 < input > /dev/null与比较dd bs=64 < input > /dev/null。bash读取该文件需要花费0.6s的时间,其中0.4 read在我的测试中花费在那些系统调用中,而其他shell在这里花费的时间要少得多。
斯特凡Chazelas

好吧,您似乎没有进行过真实的性能分析。读取调用的影响(比较不同读取大小时)约为aprox。函数readwc()trim()Burne Shell中的时间占整个时间的1%,这很可能被低估了,因为没有带libc的gprof注释mbtowc()
schily

向哪个\x扩展?
穆罕默德(Mohammad)2015年

11

这个问题是不是catecho,这是被遗忘的报价变量$i

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

$var

实际上是:

glob(split($var))

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

您可以引用变量来防止glob+split,但它不会帮助你多少,因为当外壳还需要建立大字符串参数和扫描的内容echo(更换内置echo与外置/bin/echo会给你的参数列表过长或内存不足取决于$i大小)。大多数echo实现都不符合POSIX,它将\x在收到的参数中扩展反斜杠序列。

使用cat,shell只需在每次循环迭代时生成一个进程即可cat进行复制I / O。系统还可以缓存文件内容,以使Cat处理更快。


2
@roaima:您没有提到glob部分,这可能是一个很大的原因,它对/*/*/*/*../../../../*/*/*/*/../../../../文件内容中可能存在的内容进行了映像。只想指出细节
cuonglm

谢谢你。即使没有这些,使用不带引号的变量时,时间
也会

1
time echo $( <xdditg106) >/dev/null real 0m0.125s user 0m0.085s sys 0m0.025s time echo "$( <xdditg106)" >/dev/null real 0m0.047s user 0m0.016s sys 0m0.022s
netmonk

我不知道为什么引用无法解决问题。我需要更多描述。
Mohammad

1
@ mohammad.k:正如我在回答中所写,引用变量阻止glob+split部分,它将加速while循环。我还指出,这对您没有太大帮助。从那时起,大多数shell echo行为都不符合POSIX。printf '%s' "$i"更好。
cuonglm

2

如果你打电话

i=`cat input`

这样一来,您的Shell程序就可以增加50MB到200MB(取决于内部的宽字符实现)。这可能会使您的Shell变慢,但这不是主要问题。

主要问题是上述命令需要将整个文件读入Shell内存,并且echo $i需要对.file中的文件内容进行字段拆分$i。为了进行字段拆分,需要将文件中的所有文本都转换为宽字符,这是大部分时间所花费的时间。

我用慢速情况做了一些测试,并得到了以下结果:

  • 最快的是ksh93
  • 接下来是我的Bourne Shell(比ksh93慢2倍)
  • 接下来是bash(比ksh93慢3倍)
  • 最后是ksh88(比ksh93慢7倍)

ksh93最快的原因似乎是ksh93不是mbtowc()从libc中使用而是一个自己的实现。

顺便说一句:Stephane错误地认为读取大小会产生一些影响,我编译了Bourne Shell以读取4096个字节的块而不是128个字节,并且在两种情况下都具有相同的性能。


i=`cat input`命令不执行字段拆分,而是echo $i这样做。与单独i=`cat input`相比echo $i,花费的时间可以忽略不计,但与cat input单独相比,花费的时间可以忽略不计;对于而言,bash由于bash进行少量读取,因此差异最大。从128更改为4096将不会影响的性能echo $i,但这并不是我要讲的。
斯特凡Chazelas

还要注意,echo $i根据输入内容和文件系统(如果包含IFS或glob字符)的不同,的性能会有很大的不同,这就是为什么我在答案中没有对此进行任何shell比较的原因。例如,在的输出上yes | ghead -c50M,ksh93是最慢的,但是在上yes | ghead -c50M | paste -sd: -,它是最快的。
斯特凡Chazelas

在谈论总时间时,我在谈论整个实现,是的,当然,字段拆分是通过echo命令进行的。这就是整个时间中大部分时间的花费。
2015年

您当然是正确的,其性能取决于$ i的内容。
schily

1

在这两种情况下,循环将只运行两次(一次输入单词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

echo较慢的原因之一可能是未引用的变量在空格处被拆分为单独的单词。对于50MB,这将需要大量工作。引用变量!

我建议您修复这些错误,然后重新评估您的时间安排。


我已经在本地测试过。我使用的输出创建了一个50MB的文件tar cf - | dd bs=1M count=50。我还将循环扩展了x100倍,以便将计时缩放到合理的值(我在整个代码中添加了另一个循环:for k in $(seq 100); do... done)。计时如下:

time ./1.sh

real    0m5.948s
user    0m0.012s
sys     0m0.064s

time ./2.sh

real    0m5.639s
user    0m4.060s
sys     0m0.224s

如您所见,两者之间没有真正的区别,但是如果包含的任何版本echo确实运行得更快。如果我删除引号并运行残破的版本2,则时间翻倍,这表明Shell必须要做更多的工作。

time ./2original.sh

real    0m12.498s
user    0m8.645s
sys     0m2.732s

实际上,循环运行10次,而不是两次。
fpmurphy

我照你说的做了,但是问题没有解决。cat比快得多echo。第一个脚本平均运行3秒,而第二个脚本平均运行54秒。
Mohammad

@ fpmurphy1:否 我尝试了我的代码。循环仅运行两次,而不是10次。
Mohammad

@ mohammad.k第三次:如果引用变量,问题就消失了。
roaima

@roaima:该命令tar cf - | dd bs=1M count=50做什么?它是否可以在其中包含相同字符的普通文件中使用?如果是这样,就我而言,输入文件是完全不规则的,带有各种字符和空格。再一次,我time按照您的习惯使用,结果是我说的:54秒vs 3秒。
Mohammad

-1

read 比快得多 cat

我认为每个人都可以对此进行测试:

$ cd /sys/devices/system/cpu/cpu0/cpufreq
───────────────────────────────────────────────────────────────────────────────────────────
$ time for ((i=0; i<10000; i++ )); do read p < scaling_cur_freq ; done

real    0m0.232s
user    0m0.139s
sys     0m0.088s
───────────────────────────────────────────────────────────────────────────────────────────
$ time for ((i=0; i<10000; i++ )); do cat scaling_cur_freq > /dev/null ; done

real    0m9.372s
user    0m7.518s
sys     0m2.435s
───────────────────────────────────────────────────────────────────────────────────────────
$ type -a read
read is a shell builtin
───────────────────────────────────────────────────────────────────────────────────────────
$ type -a cat
cat is /bin/cat

cat需要9.372秒。echo需要.232几秒钟。

read快40倍

我的第一次测试在$p屏幕上回显出来的read速度比快48倍cat


-2

echo是为了把1号线在屏幕上。在第二个示例中,您要做的是将文件内容放入变量中,然后打印该变量。在第一个中,您立即将内容放在屏幕上。

cat针对此用法进行了优化。echo不是。将50Mb放入环境变量中也不是一个好主意。


好奇。为什么不echo针对写出文字进行优化?
roaima

2
POSIX标准中没有任何内容表明echo可以在屏幕上放置一行。
fpmurphy

-2

这不是关于回声更快,而是关于您在做什么:

在一种情况下,您正在从输入读取并直接写入输出。换句话说,通过cat从输入中读取的任何内容,都会通过stdout进入输出。

input -> output

在另一种情况下,您将从输入读取到内存中的变量,然后将变量的内容写入输出。

input -> variable
variable -> output

后者会慢很多,尤其是在输入为50MB的情况下。


我认为您必须提到,除了从stdin复制并将其写入stdout之外,cat还必须打开文件。这是第二个脚本的出色之处,但第一个脚本的总体效果比第二个脚本好得多。
Mohammad

第二个脚本没有出色的表现。在两种情况下,cat都需要打开输入文件。在第一种情况下,cat的标准输出直接进入文件。在第二种情况下,cat的stdout首先进入变量,然后将变量打印到输出文件中。
Aleksander 2015年

@ mohammad.k,第二个脚本中强调没有“卓越”。
通配符
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.