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

Pan*_*dya 61 bash coreutils write yes

让我举个例子吧:

$ timeout 1 yes "GNU" > file1
$ wc -l file1
11504640 file1
Run Code Online (Sandbox Code Playgroud)

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

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

正如评论中所建议的,有各种技巧可以提高效率,但没有一个能与以下速度相媲美yes

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

$ timeout 1 bash -c 'while true; do echo "GNU" >> file4; done'
$ wc -l file4
18912 file4
Run Code Online (Sandbox Code Playgroud)

这些可以在一秒钟内写入多达 2 万行。它们可以进一步改进为:

$ timeout 1 bash -c 'while true; do echo "GNU"; done >> file5' 
$ wc -l file5
34517 file5
Run Code Online (Sandbox Code Playgroud)

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

这些让我们在一秒钟内达到 40,000 行。更好,但yes与每秒可写约 1100 万行的数据相去甚远!

那么,如何yes快速写入文件呢?

mik*_*erv 68

简而言之:

yes表现出类似的行为,其通常最其它标准的实用程序写入到一个文件流与输出由所述的libC经由缓冲标准输入输出。这些仅write()每隔 4kb (16kb 或 64kb)或任何输出块BUFSIZ执行一次系统调用。echo是一个write()per 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.  */
Run Code Online (Sandbox Code Playgroud)

yeswrite()之后做它自己的。


题外话:

(如最初包含在问题中并保留为上下文,可能已经写在这里的信息解释)

我试过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()系统调用只需要执行一次。这也是在下面将|管道作为内循环的输出来完成的。


直接对比:

你可能喜欢:

/* Buffer data locally once, rather than having the
large overhead of stdio buffering each item.  */
Run Code Online (Sandbox Code Playgroud)
256659456
505401
Run Code Online (Sandbox Code Playgroud)

有点像之前描述的命令子关系,但是没有管道并且子进程在后台运行,直到它杀死父进程。在这种yes情况下,自从子yes进程产生后父进程实际上已经被替换,但是 shell通过用新进程覆盖它自己的进程来调用,因此 PID 保持不变,它的僵尸子进程仍然知道要杀死谁。


更大的缓冲区:

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

for cmd in  exec\ yes 'while echo y; do :; done'
do      set +m
        sh  -c '{ sleep 1; kill "$$"; }&'"$cmd" | wc -l
        set -m
done
Run Code Online (Sandbox Code Playgroud)
1024
Run Code Online (Sandbox Code Playgroud)

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

256659456
505401
Run Code Online (Sandbox Code Playgroud)
268627968
15850496
Run Code Online (Sandbox Code Playgroud)

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


有关的:

根据要求,这里有一个比这里链接中所做的代码注释更全面的描述。

  • shell 版本正在执行 `open`(现有)`write` 和 `close`(我相信它仍在等待刷新),并为每个循环创建一个新进程并执行 `date`。 (3认同)

ori*_*ion 21

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

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

你的脚本做什么:

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

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


小智 11

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

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

real    0m0.080s
user    0m0.032s
sys     0m0.037s
Run Code Online (Sandbox Code Playgroud)

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

real    0m0.030s
user    0m0.019s
sys     0m0.011s
Run Code Online (Sandbox Code Playgroud)