没用的猫?

这可能在许多常见问题中都有提及,而不是使用:

cat file | command

(这被称为无用的使用猫) ,正确的方式应该是:

command < file

在第二种情况下,“正确”的方式-OS 不必产生额外的进程。
尽管知道这一点,我继续使用无用的猫有两个原因。

  1. 更具美感——我喜欢数据只从左向右均匀移动的时候。用其他东西(gzcatecho、 ...)替换 cat、添加第二个文件或插入新的过滤器(pvmbuffergrep...)也更容易。

  2. 我“感觉”在某些情况下它可能更快。更快,因为有两个进程,第一个(cat)执行读取,第二个执行任何操作。它们可以并行运行,这意味着有时执行速度更快。

我的逻辑正确吗(第二个原因) ?

43646 次浏览

没有!

首先,在命令中重定向发生的位置并不重要。因此,如果您希望重定向到命令的左侧,那么可以这样做:

< somefile command

command < somefile

其次,当您使用管道时,会出现 N + 1进程和子 shell。它绝对慢得多。在某些情况下,N可能为零(例如,当您重定向到一个 shell 内置程序时) ,因此使用 cat可以完全不必要地添加一个新进程。

一般来说,无论何时您发现自己在使用管道,都值得花30秒来看看是否可以消除它。(但可能不值得花超过30秒的时间。)下面是一些不必要地频繁使用管道和流程的例子:

for word in $(cat somefile); … # for word in $(<somefile); … (or better yet, while read < somefile)


grep something | awk stuff; # awk '/something/ stuff' (similar for sed)


echo something | command; # command <<< something (although echo would be necessary for pure POSIX)

欢迎编辑添加更多示例。

另一个问题是管道可以静默地屏蔽子 shell。对于这个示例,我将用 echo替换 cat,但是存在同样的问题。

echo "foo" | while read line; do
x=$line
done


echo "$x"

您可能期望 x包含 foo,但它没有。您设置的 x位于一个子 shell 中,该子 shell 生成用于执行 while循环。启动管道的 shell 中的 x有一个不相关的值,或者根本没有设置。

在 bash4中,可以配置一些 shell 选项,以便管道的最后一个命令与启动管道的命令在同一个 shell 中执行,但随后可以尝试这样做

echo "foo" | while read line; do
x=$line
done | awk '...'

x再次成为 while子 shell 的本地。

对于 UUoC 版本,cat必须将文件读入内存,然后将其写入管道,命令必须从管道中读取数据,因此内核必须复制整个文件 次,而在重定向的情况下,内核只需复制文件一次。做一件事一次比做三次快。

使用:

cat "$@" | command

是一个完全不同的,并不一定无用的使用 cat。如果命令是接受零个或多个文件名参数并依次处理它们的标准过滤器,那么它仍然是无用的。考虑一下 tr命令: 它是一个忽略或拒绝文件名参数的纯过滤器。要向它提供多个文件,必须使用如下所示的 cat。(当然,还有一个单独的讨论,即 tr的设计不是很好; 没有真正的理由说它不能被设计成一个标准的过滤器。)如果您希望命令将所有输入视为单个文件而不是多个单独的文件,那么这也可能是有效的,即使命令将接受多个单独的文件: 例如,wc就是这样一个命令。

无条件无用的是 cat single-file案例。

我认为(传统的方法)使用管道会更快一些; 在我的机器上,我使用 strace命令来查看发生了什么:

没有管道:

toc@UnixServer:~$ strace wc -l < wrong_output.c
execve("/usr/bin/wc", ["wc", "-l"], [/* 18 vars */]) = 0
brk(0)                                  = 0x8b50000
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
mmap2(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb77ad000
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
open("/etc/ld.so.cache", O_RDONLY)      = 3
fstat64(3, {st_mode=S_IFREG|0644, st_size=29107, ...}) = 0
mmap2(NULL, 29107, PROT_READ, MAP_PRIVATE, 3, 0) = 0xb77a5000
close(3)                                = 0
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
open("/lib/i386-linux-gnu/libc.so.6", O_RDONLY) = 3
read(3, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0p\222\1\0004\0\0\0"..., 512) = 512
fstat64(3, {st_mode=S_IFREG|0755, st_size=1552584, ...}) = 0
mmap2(NULL, 1563160, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0xb7627000
mmap2(0xb779f000, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x178) = 0xb779f000
mmap2(0xb77a2000, 10776, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0xb77a2000
close(3)                                = 0
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7626000
set_thread_area({entry_number:-1 -> 6, base_addr:0xb76268d0, limit:1048575, seg_32bit:1, contents:0, read_exec_only:0, limit_in_pages:1, seg_not_present:0, useable:1}) = 0
mprotect(0xb779f000, 8192, PROT_READ)   = 0
mprotect(0x804f000, 4096, PROT_READ)    = 0
mprotect(0xb77ce000, 4096, PROT_READ)   = 0
munmap(0xb77a5000, 29107)               = 0
brk(0)                                  = 0x8b50000
brk(0x8b71000)                          = 0x8b71000
open("/usr/lib/locale/locale-archive", O_RDONLY|O_LARGEFILE) = 3
fstat64(3, {st_mode=S_IFREG|0644, st_size=5540198, ...}) = 0
mmap2(NULL, 2097152, PROT_READ, MAP_PRIVATE, 3, 0) = 0xb7426000
mmap2(NULL, 1507328, PROT_READ, MAP_PRIVATE, 3, 0x2a8) = 0xb72b6000
close(3)                                = 0
open("/usr/share/locale/locale.alias", O_RDONLY) = 3
fstat64(3, {st_mode=S_IFREG|0644, st_size=2570, ...}) = 0
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb77ac000
read(3, "# Locale name alias data base.\n#"..., 4096) = 2570
read(3, "", 4096)                       = 0
close(3)                                = 0
munmap(0xb77ac000, 4096)                = 0
open("/usr/share/locale/fr_FR.UTF-8/LC_MESSAGES/coreutils.mo", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/usr/share/locale/fr_FR.utf8/LC_MESSAGES/coreutils.mo", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/usr/share/locale/fr_FR/LC_MESSAGES/coreutils.mo", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/usr/share/locale/fr.UTF-8/LC_MESSAGES/coreutils.mo", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/usr/share/locale/fr.utf8/LC_MESSAGES/coreutils.mo", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/usr/share/locale/fr/LC_MESSAGES/coreutils.mo", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/usr/share/locale-langpack/fr_FR.UTF-8/LC_MESSAGES/coreutils.mo", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/usr/share/locale-langpack/fr_FR.utf8/LC_MESSAGES/coreutils.mo", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/usr/share/locale-langpack/fr_FR/LC_MESSAGES/coreutils.mo", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/usr/share/locale-langpack/fr.UTF-8/LC_MESSAGES/coreutils.mo", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/usr/share/locale-langpack/fr.utf8/LC_MESSAGES/coreutils.mo", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/usr/share/locale-langpack/fr/LC_MESSAGES/coreutils.mo", O_RDONLY) = 3
fstat64(3, {st_mode=S_IFREG|0644, st_size=316721, ...}) = 0
mmap2(NULL, 316721, PROT_READ, MAP_PRIVATE, 3, 0) = 0xb7268000
close(3)                                = 0
open("/usr/lib/i386-linux-gnu/gconv/gconv-modules.cache", O_RDONLY) = 3
fstat64(3, {st_mode=S_IFREG|0644, st_size=26064, ...}) = 0
mmap2(NULL, 26064, PROT_READ, MAP_SHARED, 3, 0) = 0xb7261000
close(3)                                = 0
read(0, "#include<stdio.h>\n\nint main(int "..., 16384) = 180
read(0, "", 16384)                      = 0
fstat64(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 2), ...}) = 0
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7260000
write(1, "13\n", 313
)                     = 3
close(0)                                = 0
close(1)                                = 0
munmap(0xb7260000, 4096)                = 0
close(2)                                = 0
exit_group(0)                           = ?

用烟斗:

toc@UnixServer:~$ strace cat wrong_output.c | wc -l
execve("/bin/cat", ["cat", "wrong_output.c"], [/* 18 vars */]) = 0
brk(0)                                  = 0xa017000
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
mmap2(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb774b000
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
open("/etc/ld.so.cache", O_RDONLY)      = 3
fstat64(3, {st_mode=S_IFREG|0644, st_size=29107, ...}) = 0
mmap2(NULL, 29107, PROT_READ, MAP_PRIVATE, 3, 0) = 0xb7743000
close(3)                                = 0
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
open("/lib/i386-linux-gnu/libc.so.6", O_RDONLY) = 3
read(3, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0p\222\1\0004\0\0\0"..., 512) = 512
fstat64(3, {st_mode=S_IFREG|0755, st_size=1552584, ...}) = 0
mmap2(NULL, 1563160, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0xb75c5000
mmap2(0xb773d000, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x178) = 0xb773d000
mmap2(0xb7740000, 10776, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0xb7740000
close(3)                                = 0
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb75c4000
set_thread_area({entry_number:-1 -> 6, base_addr:0xb75c48d0, limit:1048575, seg_32bit:1, contents:0, read_exec_only:0, limit_in_pages:1, seg_not_present:0, useable:1}) = 0
mprotect(0xb773d000, 8192, PROT_READ)   = 0
mprotect(0x8051000, 4096, PROT_READ)    = 0
mprotect(0xb776c000, 4096, PROT_READ)   = 0
munmap(0xb7743000, 29107)               = 0
brk(0)                                  = 0xa017000
brk(0xa038000)                          = 0xa038000
open("/usr/lib/locale/locale-archive", O_RDONLY|O_LARGEFILE) = 3
fstat64(3, {st_mode=S_IFREG|0644, st_size=5540198, ...}) = 0
mmap2(NULL, 2097152, PROT_READ, MAP_PRIVATE, 3, 0) = 0xb73c4000
mmap2(NULL, 1507328, PROT_READ, MAP_PRIVATE, 3, 0x2a8) = 0xb7254000
close(3)                                = 0
fstat64(1, {st_mode=S_IFIFO|0600, st_size=0, ...}) = 0
open("wrong_output.c", O_RDONLY|O_LARGEFILE) = 3
fstat64(3, {st_mode=S_IFREG|0664, st_size=180, ...}) = 0
read(3, "#include<stdio.h>\n\nint main(int "..., 32768) = 180
write(1, "#include<stdio.h>\n\nint main(int "..., 180) = 180
read(3, "", 32768)                      = 0
close(3)                                = 0
close(1)                                = 0
close(2)                                = 0
exit_group(0)                           = ?
13

您可以使用 stracetime命令进行一些测试,使用更多和更长的命令进行良好的基准测试。

直到今天我才知道这个奖项,因为有个新人想把我的一个答案扣在我头上。是 cat file.txt | grep foo | cut ... | cut ...。我给了他一个我的想法,只有在这样做之后,访问他给我的链接,涉及的起源和这样做的做法。进一步的搜索引出了这个问题。有点不幸的是,尽管有意识的考虑,没有一个答案包括我的理由。

我并不想在回答他的问题时有所防备。毕竟,在我年轻的时候,我会把命令写成 grep foo file.txt | cut ... | cut ...,因为无论什么时候,只要你经常使用单个 grep,你就会知道文件参数的位置,并且已经知道第一个是模式,后面的是文件名。

当我回答这个问题时,使用 cat是一个有意识的选择,部分原因是出于“好品味”(用 Linus Torvalds 的话说) ,但主要是出于一个令人信服的功能原因。

后一个原因更重要,所以我会先把它放出来。当我提供一个管道作为解决方案时,我希望它是可重用的。很有可能在另一条管道的末端添加一条管道或将其拼接到另一条管道中。在这种情况下,有一个要 grep 的文件参数会影响重用性,而且如果文件参数存在的话,很有可能在没有错误消息的情况下使用 悄悄地。例如,grep foo xyz | grep bar xyz | wc会告诉你在 xyz中有多少行包含 bar,而你期望的是同时包含 foobar的行的数目。在使用管道中的命令之前,必须将参数更改为该命令,这很容易出错。再加上无声失败的可能性,这就成了一种特别阴险的做法。

前一个原因也不是不重要,因为很多“ 品味不错” 仅仅是一个直观的潜意识理论,比如上面提到的无声的失败,你不能想到正当某个需要教育的人说“但是那只猫不是没用吗”的时候。

不过,我也会尽量做到意识到前面提到的“好品味”的原因。这个原因与 Unix 的正交设计精神有关。grep不是 cutls不是 grep。因此,至少 grep foo file1 file2 file3违背了设计精神。正交法是 cat file1 file2 file3 | grep foo。现在,grep foo file1仅仅是 grep foo file1 file2 file3的一个特例,如果你不同样对待它,你至少是在用尽大脑的生物钟周期,试图避免无用的猫奖。

这导致我们的论点,grep foo file1 file2 file3是连接,和 cat连接,所以它是适当的 cat file1 file2 file3,但因为 cat没有连接在 cat file1 | grep foo,因此我们违反了精神的 cat和万能的 Unix。如果是这样的话,那么 Unix 将需要一个不同的命令来读取一个文件的输出并将其吐出到 stdout (而不是对其进行分页,或者只是将其纯粹吐出到 stdout)。所以你会遇到这样的情况,你说 cat file1 file2或者你说 dog file1,并且认真记住避免 cat file1以避免获奖,同时也避免 dog file1 file2,因为如果指定了多个文件,希望 cat0的设计会抛出一个错误。

希望在这一点上,您同情 Unix 设计者没有包含一个单独的命令来向标准输出文件,同时还将 cat命名为连接,而不是给它一些其他的名称。<edit>删除了对 <的不正确注释,事实上,<是一个有效的非拷贝工具,可以将文件吐到标准输出(stdout) ,你可以在管道的开始位置放置这个文件,所以 Unix 设计师确实为这个 </edit>包含了一些特别的东西

下一个问题是,为什么命令只是将一个文件或几个文件连接到标准输出,而不需要进行任何进一步的处理,这一点很重要?其中一个原因是避免让每个操作标准输入的 Unix 命令都知道如何解析至少一个命令行文件参数,并在其存在时将其用作输入。第二个原因是为了避免用户必须记住: (a)文件名参数的位置; (b)避免上面提到的静默管道错误。

这就引出了为什么 grep确实有额外的逻辑。其基本原理是允许用户流利地使用经常使用的命令,并且以 独立的为基础(而不是作为管道)。它是正交性的一个轻微的妥协,在可用性方面获得了显著的收益。并不是所有的命令都应该这样设计,并且不经常使用的命令应该完全避免文件参数的额外逻辑(记住额外的逻辑会导致不必要的脆弱性(bug 的可能性))。例外情况是允许像 grep这样的文件参数。(顺便说一下,请注意,ls有一个完全不同的理由,不仅接受,而且几乎需要文件参数)

最后,如果像 grep(但不一定是 ls)这样的异常命令在指定文件参数时也可以使用标准输入,那么可以做得更好。

我不同意大多数过分沾沾自喜的 UUOC 奖的例子,因为在教别人的时候,cat是任何命令的方便替代品,或者是任何生成适合讨论中的问题或任务的输出的复杂命令管道。

在 Stack Overflow、 ServerFault、 Unix & Linux 或任何 SE 站点上尤其如此。

如果有人特别问及优化,或者如果你想添加额外的信息,那么,很好,谈谈使用 cat 是如何效率低下的。但是,不要责备人们,因为他们选择了简单和易于理解的目标在他们的例子中,而不是看着我如何酷我!复杂性。

总之,因为猫并不总是猫。

还因为大多数喜欢到处给 UUOC 颁奖的人这么做是因为他们更关心炫耀自己有多“聪明”,而不是帮助或教导别人。实际上,他们证明了自己可能只是另一个新手,只是找到了一根小小的棍子来打败同龄人。


更新

这是我在 https://unix.stackexchange.com/a/301194/7696上发布的另一个 UUOC 的回答:

sqlq() {
local filter
filter='cat'


# very primitive, use getopts for real option handling.
if [ "$1" == "--delete-blank-lines" ] ; then
filter='grep -v "^$"'
shift
fi


# each arg is piped into sqlplus as a separate command
printf "%s\n" "$@" | sqlplus -S sss/eee@sid | $filter
}

UUOC 学究会说,这是一个 UUOC,因为它很容易使 $filter默认为空字符串,让 if语句做 filter='| grep -v "^$"',但是 IMO,通过没有嵌入管道字符在 $filter,这个“无用的”cat服务于极其有用的目的,自我记录的事实,printf行上的 $filter不只是 sqlplus的另一个参数,它是一个可选的用户选择的输出过滤器。

如果需要多个可选的输出过滤器,选项处理可以根据需要经常将 | whatever附加到 $filter-管道中的一个额外的 cat不会损害任何东西或引起任何明显的性能损失。

作为一个经常指出这个问题以及其他一些 shell 编程反模式的人,我觉得有必要加入进来,虽然有点迟。

Shell 脚本是一种复制/粘贴语言。对于大多数编写 shell 脚本的人来说,他们并不是为了学习这门语言; 这只是他们必须克服的一个障碍,以便继续用他们实际上比较熟悉的语言来做事情。

在这种情况下,我认为传播各种 shell 脚本反模式具有破坏性,甚至可能具有破坏性。人们在 Stack Overflow 上找到的代码理想情况下应该可以复制/粘贴到他们的环境中,只需要进行最小的更改和不完全的理解。

在网上众多的 shell 脚本资源中,Stack Overflow 是不同寻常的,因为用户可以通过编辑站点上的问题和答案来帮助塑造站点的质量。但是,代码编辑是有问题的是因为它很容易进行代码作者不希望进行的更改。因此,我们倾向于留下注释来建议对代码的更改。

UUCA 和相关的反模式注释不仅仅针对我们所注释的代码的作者; 它们也是一个 买者自负,可以帮助站点的 读者意识到他们在这里找到的代码中的问题。

我们不能指望在 Stack Overflow 上没有答案推荐无用的 cat(或者未引用的变量,或者 chmod 777,或者其他大量的反模式瘟疫) ,但是我们至少可以帮助教育那些将要把这段代码复制/粘贴到他们脚本最内部的紧密循环中的用户,这个循环执行了数百万次。

就技术原因而言,传统的智慧是我们应该尽量减少外部进程的数量; 在编写 shell 脚本时,这仍然是一个很好的通用指南。

防守的猫:

是的,

   < input process > output

或者

   process < input > output

更有效,但是许多调用没有性能问题,所以您不会在意。

人体工学原因:

我们习惯于从左往右读,所以命令如

    cat infile | process1 | process2 > outfile

是微不足道的。

    process1 < infile | process2 > outfile

必须跳过 process1,然后从左读到右。这可以通过以下方法修复:

    < infile process1 | process2 > outfile

看起来,好像有一个箭头指向左边,那里什么都没有。更令人困惑的,看起来像是花哨的引用是:

    process1 > outfile < infile

生成脚本通常是一个迭代的过程,

    cat file
cat file | process1
cat file | process1 | process2
cat file | process1 | process2 > outfile

你在哪里看到你的进步,而逐步

    < file

简单的方法更不容易出错,而且对于 cat 来说,人机工程学的命令连接也很简单。

另一个话题是,大多数人在使用计算机之前很久就已经接触过 > 和 < 作为比较运算符,当他们以程序员的身份使用计算机时,他们更经常接触到这些运算符。

用 < 和 > 比较两个操作数是对位交换的,这意味着

(a > b) == (b < a)

我记得第一次使用 < for 输入重定向时,我很害怕

a.sh < file

可能意味着

file > a.sh

然后改写我的 ASH 脚本,也许这对很多初学者来说都是个问题。

罕见的差异

wc -c journal.txt
15666 journal.txt
cat journal.txt | wc -c
15666

后者可以直接用于计算。

factor $(cat journal.txt | wc -c)

当然,< 也可以在这里使用,而不是 file 参数:

< journal.txt wc -c
15666
wc -c < journal.txt
15666
    

但谁在乎呢,一万五?

如果我偶尔遇到问题,我一定会改变我叫猫的习惯。

当使用非常大或许多,许多文件,避免猫是好的。对于大多数问题来说,使用 cat 是正交的,离题的,而不是问题。

在每一个 shell 主题上开始这些毫无用处的猫讨论只会让人感到厌烦和无聊。在处理表现问题时,要有自己的生活,等待成名的那一分钟。

我经常在例子中使用 cat file | myprogram。有时,我被指责无用的使用猫(http://porkmail.org/era/unix/award.html)。我不同意,理由如下:

  • 很容易理解正在发生的事情。

    在读取 UNIX 命令时,您希望读取后跟参数、后跟重定向的命令。将重定向放在任何地方都是可能的,但是它很少被看到——因此人们将很难阅读示例。我相信

    cat foo | program1 -o option -b option | program2
    

    读起来比较容易

    program1 -o option -b option < foo | program2
    

    如果将重定向移动到开始位置,会让不习惯这种语法的人感到困惑:

    < foo program1 -o option -b option | program2
    

    例子应该容易理解。

  • 改变很容易。

    如果知道程序可以从 cat读取,那么通常可以假定它可以从任何输出到 STDOUT 的程序中读取输出,因此可以根据自己的需要对其进行调整,并获得可预测的结果。

  • 它强调,如果 STDIN 不是一个文件,程序不会失败。

    假设如果 program1 < foo工作,那么 cat foo | program1也将工作,这是不安全的。然而,可以有把握地假设情况正好相反。如果 STDIN 是一个文件,这个程序可以工作,但是如果输入是一个管道,这个程序就会失败,因为它使用了 find:

    # works
    < foo perl -e 'seek(STDIN,1,1) || die;print <STDIN>'
    
    
    # fails
    cat foo | perl -e 'seek(STDIN,1,1) || die;print <STDIN>'
    

Performance cost

There is a cost of doing the additional cat. To give an idea of how much I ran a few tests to simulate baseline (cat), low throughput (bzip2), medium throughput (gzip), and high throughput (grep).

cat $ISO | cat
< $ISO cat
cat $ISO | bzip2
< $ISO | bzip2
cat $ISO | gzip
< $ISO gzip
cat $ISO | grep no_such_string
< $ISO grep no_such_string

测试在一个低端系统(0.6 GHz)和一个普通笔记本电脑(2.2 GHz)上运行。他们在每个系统上运行了10次,并选择了最佳时机来模拟每个测试的最佳情况。$ISO 是 ubuntu-11.04-desk-i386。等等。 (这里的桌子更漂亮: http://oletange.blogspot.com/2013/10/useless-use-of-cat.html)

CPU                       0.6 GHz ARM
Command                   cat $ISO|                        <$ISO                            Diff                             Diff (pct)
Throughput \ Time (ms)    User       Sys        Real       User       Sys        Real       User       Sys        Real       User       Sys        Real
Baseline (cat)                     55      14453      33090         23       6937      33126         32       7516        -36        239        208         99
Low (bzip2)                   1945148      16094    1973754    1941727       5664    1959982       3420      10430      13772        100        284        100
Medium (gzip)                  413914      13383     431812     407016       5477     416760       6898       7906      15052        101        244        103
High (grep no_such_string)      80656      15133      99049      79180       4336      86885       1476      10797      12164        101        349        114


CPU                       Core i7 2.2 GHz
Command                   cat $ISO|           <$ISO             Diff          Diff (pct)
Throughput \ Time (ms)    User     Sys Real   User   Sys Real   User Sys Real User       Sys Real
Baseline (cat)                    0 356    215      1  84     88    0 272  127          0 423  244
Low (bzip2)                  136184 896 136765 136728 160 137131 -545 736 -366         99 560   99
Medium (gzip)                 26564 788  26791  26332 108  26492  232 680  298        100 729  101
High (grep no_such_string)      264 392    483    216  84    304   48 308  179        122 466  158

结果表明,对于中低吞吐量,成本在1% 左右。这完全在测量的不确定性范围内,因此在实践中没有差别。

对于高吞吐量,差异更大,两者之间有明显的差异。

由此得出结论: 如果:

  • 处理的复杂性类似于简单的 grep
  • 性能比可读性更重要。

否则,使用 <还是 cat |都无关紧要。

因此,只有当且仅当:

  • 你可以测量表现的显著差异(在颁奖时公布你的测量结果)
  • 性能比可读性更重要。