为什么打开文件比读取可变内容更快?

des*_*ert 36 shell bash zsh shell-script variable

bash脚本中,我需要来自/proc/文件的各种值。到现在为止,我有几十行像这样直接 grep 文件:

grep -oP '^MemFree: *\K[0-9]+' /proc/meminfo
Run Code Online (Sandbox Code Playgroud)

为了提高效率,我将文件内容保存在一个变量中并进行了搜索:

a=$(</proc/meminfo)
echo "$a" | grep -oP '^MemFree: *\K[0-9]+'
Run Code Online (Sandbox Code Playgroud)

而不是多次打开文件,这应该只打开一次并grep变量内容,我认为这会更快 - 但实际上它更慢:

grep -oP '^MemFree: *\K[0-9]+' /proc/meminfo
Run Code Online (Sandbox Code Playgroud)

dash和也是如此zsh。我怀疑/proc/文件的特殊状态是一个原因,但是当我将 的内容复制/proc/meminfo到常规文件并使用它时,结果是相同的:

a=$(</proc/meminfo)
echo "$a" | grep -oP '^MemFree: *\K[0-9]+'
Run Code Online (Sandbox Code Playgroud)

使用 here 字符串来保存管道使其稍微快一点,但仍然不如文件快:

bash 4.4.19 $ time for i in {1..1000};do grep ^MemFree /proc/meminfo;done >/dev/null
real    0m0.803s
user    0m0.619s
sys     0m0.232s
bash 4.4.19 $ a=$(</proc/meminfo)
bash 4.4.19 $ time for i in {1..1000};do echo "$a"|grep ^MemFree; done >/dev/null
real    0m1.182s
user    0m1.425s
sys     0m0.506s
Run Code Online (Sandbox Code Playgroud)

为什么打开文件比从变量读取相同内容更快?

Sté*_*las 48

在这里,它不是打开一个文件读取变量的内容,但更多的分叉额外的进程中。

grep -oP '^MemFree: *\K[0-9]+' /proc/meminfofork 一个执行grep打开的进程/proc/meminfo(一个虚拟文件,在内存中,不涉及磁盘 I/O)读取它并匹配正则表达式。

其中最昂贵的部分是分叉进程并加载 grep 实用程序及其库依赖项、进行动态链接、打开语言环境数据库、磁盘上的数十个文件(但可能缓存在内存中)。

/proc/meminfo相比之下,关于阅读的部分是微不足道的,内核在那里生成信息grep所需的时间很少,阅读它的时间也很少。

如果您运行strace -c它,您会看到用于读取的一个open()和一个read()系统调用/proc/meminfogrep启动时strace -c所做的一切相比是花生(不计算分叉)。

在:

a=$(</proc/meminfo)
Run Code Online (Sandbox Code Playgroud)

在大多数支持该$(<...)ksh 运算符的 shell 中,shell 只是打开文件并读取其内容(并去除尾随的换行符)。bash是不同的并且效率低得多,因为它分叉了一个进程来进行读取并通过管道将数据传递给父进程。但在这里,它已经完成了一次,所以没关系。

在:

printf '%s\n' "$a" | grep '^MemFree'
Run Code Online (Sandbox Code Playgroud)

shell 需要产生两个进程,这两个进程同时运行,但通过管道相互交互。管道的创建、拆除、写入和读取成本很低。更大的成本是产生一个额外的进程。进程的调度也有一些影响。

您可能会发现使用 zsh<<<运算符可以稍微快一点:

grep '^MemFree' <<< "$a"
Run Code Online (Sandbox Code Playgroud)

在 zsh 和 bash 中,这是通过将 的内容写入$a临时文件来完成的,这比产生额外的进程要便宜,但与直接获取数据相比,可能不会给您带来任何好处/proc/meminfo。这仍然比您/proc/meminfo在磁盘上复制的方法效率低,因为临时文件的写入是在每次迭代时完成的。

dash不支持here-strings,但它的heredocs 是用不涉及产生额外进程的管道实现的。在:

 grep '^MemFree' << EOF
 $a
 EOF
Run Code Online (Sandbox Code Playgroud)

外壳创建一个管道,派生一个进程。子进程grep以其标准输入作为管道的读取端执行,父进程在管道的另一端写入内容。

但是管道处理和进程同步仍然可能比直接获取数据更昂贵/proc/meminfo

内容/proc/meminfo较短,制作时间不长。如果您想节省一些 CPU 周期,您需要移除昂贵的部分:分叉进程和运行外部命令。

喜欢:

IFS= read -rd '' meminfo < /proc/meminfo
memfree=${meminfo#*MemFree:}
memfree=${memfree%%$'\n'*}
memfree=${memfree#"${memfree%%[! ]*}"}
Run Code Online (Sandbox Code Playgroud)

避免bash虽然其模式匹配非常低效。使用zsh -o extendedglob,您可以将其缩短为:

memfree=${${"$(</proc/meminfo)"##*MemFree: #}%%$'\n'*}
Run Code Online (Sandbox Code Playgroud)

请注意,这^在许多 shell(至少带有扩展全局选项的 Bourne、fish、rc、es 和 zsh)中很特别,我建议引用它。另请注意,echo不能用于输出任意数据(因此我使用了printf上述内容)。

  • @DavidConrad 确实如此,但大多数 shell 不会尝试分析管道,它*可以*在当前进程中运行哪些部分。它只是自己分叉,让孩子们弄清楚。在这种情况下,父进程分叉了两次;左侧的孩子然后看到一个内置并执行它;右边的孩子看到`grep`和execs。 (6认同)
  • 在使用 `printf` 的情况下,你说 shell 需要产生两个进程,但是 `printf` 不是内置的 shell 吗? (4认同)
  • @D.BenKnoble,我并不是想暗示`bash` 不支持`&lt;&lt;&lt;`,只是操作符来自`zsh`,就像`$(&lt;...)` 来自ksh。 (2认同)

Prv*_*dav 6

在您的第一种情况下,您只是使用 grep 实用程序并从 file 中查找某些内容/proc/meminfo,它/proc是一个虚拟文件系统,因此/proc/meminfofile 在内存中,并且它需要很少的时间来获取其内容。

但是在第二种情况下,您正在创建一个管道,然后使用此管道将第一个命令的输出传递给第二个命令,这很昂贵。

区别是因为/proc(因为它在内存中)和管道,看下面的例子:

time for i in {1..1000};do grep ^MemFree /proc/meminfo;done >/dev/null

real    0m0.914s
user    0m0.032s
sys     0m0.148s


cat /proc/meminfo > file
time for i in {1..1000};do grep ^MemFree file;done >/dev/null

real    0m0.938s
user    0m0.032s
sys     0m0.152s


time for i in {1..1000};do echo "$a"|grep ^MemFree; done >/dev/null

real    0m1.016s
user    0m0.040s
sys     0m0.232s
Run Code Online (Sandbox Code Playgroud)