是的如何快速写入文件?


58

让我举个例子吧:

$ 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写入了11504640行,而我只能1953使用bash for和在5秒内写入行echo

正如评论中所建议的那样,有多种技巧可以使其更高效,但没有一个能与之匹敌的速度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

这些使我们在一秒钟之内可以达到4万行。更好,但仍然相差甚远,yes每秒可以写约1100万行!

那么,如何yes这么快地写入文件?



9
在第二个示例中,循环的每次迭代都有两个外部命令调用,而且date有些繁重,而且外壳程序必须echo为每次循环迭代重新打开输出流。在第一个示例中,只有一个命令调用和一个输出重定向,并且该命令非常轻巧。两者绝不是可比的。
CVn

@MichaelKjörling,您说得对,date可能很重,请参阅我的问题的编辑。
潘迪

1
timeout 1 $(while true; do echo "GNU">>file2; done;)这是错误的使用方式,timeout 因为timeout命令仅在命令替换完成后才会启动。使用timeout 1 sh -c 'while true; do echo "GNU">>file2; done'
muru

1
答案摘要:write(2)在第一个示例中,它仅将CPU时间花费在系统调用上,而不是在其他系统调用的负载,shell开销甚至进程创建上花费(运行并等待date打印到文件的每一行)。在具有大量RAM的现代系统上,写一秒钟的时间几乎不足以使其成为磁盘I / O(而不是CPU /内存)的瓶颈。如果允许运行更长的时间,差异将会更小。(取决于您使用的bash实现有多糟糕,以及CPU和磁盘的相对速度,您甚至可能不会使bash使磁盘I / O饱和)。
彼得·科德斯

Answers:


65

简而言之:

yes表现出类似的行为,其通常最其它标准的实用程序写入到一个文件流与输出经由由缓冲的libC STDIO。这些仅write()每4kb (16kb或64kb)或任何输出块BUFSIZ执行syscall 。echowrite()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.  */

yeswrite()此后自行执行。


题外话:

(最初包含在问题中,并保留在此处已经提供的可能是翔实的解释的上下文中)

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

timeout您在使用命令替换时遇到的问题-我想我现在明白了,可以解释为什么它没有停止。timeout不会启动,因为它的命令行永远不会运行。您的外壳会派生一个子外壳,在其stdout上打开一个管道,然后读取它。当孩子退出时,它将停止阅读,然后它将解释孩子写的所有用于$IFS破坏和glob扩展的单词,结果将替换从$(到匹配的所有内容)

但是,如果子进程是一个永不循环的永不写入管道的循环,则子进程永远不会停止循环,并且(在我看来)您执行并杀死子循环timeout之前,其命令行永远不会完成。因此, 永远无法终止需要启动的循环。 CTRL-Ctimeout


其他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情况下,自从生成孩子以来,实际上已经替换了父对象,但是shell yes通过将其自身的进程与新的对象叠加来进行调用,因此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


有关:

根据要求,对于此链接在此处所做的操作,有比仅是代码注释更全面的描述。


@heemayl-也许吗?我不是很确定我明白你的要求吗?当程序使用stdio写入输出时,它将不进行缓冲(默认情况下为stderr)或行缓冲(默认情况下为端子)或块缓冲(基本上大多数其他内容默认情况下都设置)。我对设置输出缓冲区大小的方法还不太清楚-但通常为4kb。因此stdio lib函数将收集其输出,直到它们可以写入整个块为止。dd是一种标准工​​具,例如绝对不使用stdio。大多数其他人都这样做。
mikeserv '16

3
Shell版本正在执行open(现有)writeAND close(我相信仍在等待刷新),并date为每个循环创建一个新进程并执行。
dave_thompson_085 '16

@ dave_thompson_085 -去的/ dev /聊天。正如您在那看到的,您所说的不一定是正确的。例如,对我来说执行该wc -l循环bash将获得该sh循环的输出的1/5- bash管理100k writes()dash500k的一点点。
mikeserv '16

对不起,我模棱两可;我的意思是问题中的外壳版本,在我阅读该外壳版本时,它仅具有原始版本,for((sec0=`date +%S`;...可以控制循环中的时间和重定向,而没有后续的改进。
dave_thompson_085 '16

@ dave_thompson_085-很好。无论如何,答案在某些基本点上都是错误的,正如我希望的那样,现在应该是非常正确的。
mikeserv '16

20

一个更好的问题是为什么您的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

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

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

是的,这很重要,在我看来,写作速度(至少)要翻倍
Pandya
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.