“yes”如何如此快地写入文件?

“yes”如何如此快地写入文件?

让我举个例子吧:

$ timeout 1 yes "GNU" > file1
$ wc -l file1
11504640 file1

$ for ((sec0=`date +%S`;sec<=$(($sec0+5));sec=`date +%S`)); do echo "GNU" >> file2; done
$ wc -l file2
1953 file2

在这里您可以看到该命令在一秒钟内yes写入行,而我使用 bash和11504640只能在 5 秒内写入行。1953forecho

正如评论中所建议的,有各种技巧可以提高效率,但没有一个能接近匹配的速度yes

$ ( while :; do echo "GNU" >> file3; done) & pid=$! ; sleep 1 ; kill $pid
[1] 3054
$ wc -l file3
19596 file3

$ timeout 1 bash -c 'while true; do echo "GNU" >> file4; done'
$ wc -l file4
18912 file4

它们每秒最多可以写入 2 万行。它们还可以进一步改进为:

$ timeout 1 bash -c 'while true; do echo "GNU"; done >> file5' 
$ wc -l file5
34517 file5

$ ( while :; do echo "GNU"; done >> file6 ) & pid=$! ; sleep 1 ; kill $pid
[1] 5690
$ wc -l file6
40961 file6

这些让我们在一秒钟内达到 40,000 行。更好了,但yes距离每秒写 1100 万行还很远!

所以,怎么yes这么快写入文件?

答案1

简而言之:

yes表现出与大多数其他标准实用程序类似的行为,这些实用程序通常到一个文件流输出由 libC 通过缓冲stdio。这些仅执行系统调用write()每约 4kb(16kb 或 64kb)或任何输出块BUFSIZ是 。echo是一个write()GNU。那是一个很多模式切换 (显然,这不像上下文切换

更不用说,除了最初的优化循环之外,yes它是一个非常简单、微小的编译 C 循环,并且您的 shell 循环根本无法与编译器优化的程序相比。


但是我错了:

当我之前说yes使用时stdio,我只是假设它确实如此,因为它的行为很像那些这样做的人。这是不正确的——它只是以这种方式模仿他们的行为。它实际上所做的事情非常类似于我在下面使用 shell 所做的事情:它首先循环合并其参数(或者y如果没有的话)直到它们不再生长而不超过BUFSIZ

来自的评论来源紧接在相关for循环状态之前:

/* Buffer data locally once, rather than having the
large overhead of stdio buffering each item.  */

yes此后执行自己的write()操作。


题外话:

(最初包含在问题中并保留作为此处已写的可能提供信息的解释的上下文):

我已经尝试过timeout 1 $(while true; do echo "GNU">>file2; done;)但无法停止循环。

您在命令替换方面遇到的问题timeout- 我想我现在明白了,并且可以解释为什么它不会停止。timeout不会启动,因为它的命令行从未运行。您的 shell 分叉一个子 shell,在其标准输出上打开一个管道并读取它。当子进程退出时,它将停止读取,然后它将解释为$IFS重整和全局扩展而编写的所有子进程,并根据结果替换从$(到 匹配的所有内容)

但是,如果子进程是一个无限循环,永远不会写入管道,那么子进程就永远不会停止循环,并且timeout命令行永远不会在(正如我猜测的那样)你执行Ctrl+C并杀死子循环。所以timeout可以 绝不杀死需要在开始之前完成的循环。


其他timeout的:

...与性能问题的相关性并不像 shell 程序必须花费在用户模式和内核模式之间切换来处理输出的时间那样重要。timeout不过,它并不像 shell 那样灵活:shell 的优势在于其处理参数和管理其他进程的能力。

正如其他地方所指出的,只需移动您的[fd-num] >> named_file重定向到循环的输出目标而不是仅将循环命令的输出定向到那里可以显着提高性能,因为这样至少open()系统调用只需完成一次。下面也使用|目标为内部循环输出的管道来完成此操作。


直接比较:

你可能会喜欢:

for cmd in  exec\ yes 'while echo y; do :; done'
do      set +m
        sh  -c '{ sleep 1; kill "$$"; }&'"$cmd" | wc -l
        set -m
done
256659456
505401

这是种类就像前面描述的命令子关系一样,但是没有管道,并且子进程处于后台状态,直到它杀死父进程。在这种yes情况下,自子进程产生以来,父进程实际上已被替换,但 shellyes通过用新进程覆盖其自己的进程来调用,因此 PID 保持不变,并且其僵尸子进程仍然知道要杀死谁。


更大的缓冲区:

现在让我们看看如何增加 shell 的write()缓冲区。

IFS="
";    set y ""              ### sets up the macro expansion       
until [ "${512+1}" ]        ### gather at least 512 args
do    set "$@$@";done       ### exponentially expands "$@"
printf %s "$*"| wc -c       ### 1 write of 512 concatenated "y\n"'s  
1024

我选择这个数字是因为任何超过 1kb 的输出字符串都会被分割成单独的write()。所以这又是一个循环:

for cmd in 'exec  yes' \
           'until [ "${512+:}" ]; do set "$@$@"; done
            while printf %s "$*"; do :; done'
do      set +m
        sh  -c $'IFS="\n"; { sleep 1; kill "$$"; }&'"$cmd" shyes y ""| wc -l
        set -m
done
268627968
15850496

在本次测试中,在相同时间内 shell 写入的数据量是上次测试的 300 倍。不是太寒酸。但事实并非如此yes


费了:

根据要求,这里有比单纯的代码注释更全面的描述,位于这个链接

答案2

更好的问题是为什么你的 shell 写入文件的速度这么慢。任何负责任地使用文件写入系统调用(而不是一次刷新每个字符)的独立编译程序都会相当快地完成它。你正在做的就是在一个解释的语言(外壳),此外你还做了一个很多不必要的输入输出操作。什么yes是:

  • 打开一个文件进行写入
  • 调用优化和编译的函数来写入流
  • 流被缓冲,因此系统调用(到内核模式的昂贵切换)很少发生,大块
  • 关闭一个文件

你的脚本的作用:

  • 读入一行代码
  • 解释代码,进行大量额外操作来实际解析您的输入并找出要做什么
  • 对于 while 循环的每次迭代(这在解释语言中可能并不便宜):
    • 调用date外部命令并存储其输出(仅在原始版本中 - 在修订版本中,如果不这样做,您将获得 10 倍的增益)
    • 测试循环的终止条件是否满足
    • 打开处于追加模式的文件
    • 解析echo命令,将其(使用一些模式匹配代码)识别为 shell 内置命令,调用参数扩展以及参数“GNU”上的其他所有内容,最后将该行写入打开的文件
    • 关闭再次该文件
    • 重复这个过程

昂贵的部分:整个解释非常昂贵(bash 对所有输入进行了大量的预处理 - 你的字符串可能包含变量替换、进程替换、大括号扩展、转义字符等等),内置函数的每次调用都是可能是一个 switch 语句,重定向到处理内置函数的函数,非常重要的是,您为每一行输出打开和关闭一个文件。你可以把它放在>> filewhile 循环之外来实现快很多,但你仍然使用解释性语言。你很幸运,这echo是一个内置的 shell,而不是外部命令 - 否则,你的循环将涉及在每次迭代时创建一个新进程(fork 和 exec)。这会使进程停止——当你date在循环中使用命令时,你会看到这是多么昂贵。

答案3

其他答案已经说到了要点。附带说明一下,您可以通过在计算结束时写入输出文件来增加 while 循环的吞吐量。比较:

$ i=0;time while  [ $i -le 1000 ]; do ((++i)); echo "GNU" >>/tmp/f; done;

real    0m0.080s
user    0m0.032s
sys     0m0.037s

$ i=0;time while  [ $i -le 1000 ]; do ((++i)); echo "GNU"; done>>/tmp/f;

real    0m0.030s
user    0m0.019s
sys     0m0.011s

相关内容