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


36

bash脚本中,我需要/proc/文件中的各种值。到目前为止,我有数十行代码直接像这样对文件进行grep:

grep -oP '^MemFree: *\K[0-9]+' /proc/meminfo

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

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

与其多次打开文件,不如将其打开一次并grep变量内容,我认为这会更快-但实际上它更慢:

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

同样是真正的dashzsh。我怀疑/proc/文件的特殊状态是有原因的,但是当我将文件的内容复制/proc/meminfo到常规文件并使用相同的结果时:

bash 4.4.19 $ cat </proc/meminfo >meminfo
bash 4.4.19 $ time for i in $(seq 1 1000);do grep ^MemFree meminfo; done >/dev/null
real    0m0.790s
user    0m0.608s
sys     0m0.227s

使用here字符串保存管道会使它稍快一些,但仍不如文件快:

bash 4.4.19 $ time for i in $(seq 1 1000);do <<<"$a" grep ^MemFree; done >/dev/null
real    0m0.977s
user    0m0.758s
sys     0m0.268s

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


@ l0b0这个假设不是错误的,问题说明了我是如何提出的,答案解释了为什么是这种情况。现在,您所做的编辑使答案不再回答标题问题:他们没有说是这样。
甜点

好,澄清。因为在大多数情况下标题是错误的,所以对于某些内存映射的特殊文件而言并非如此。
l0b0

@ l0b0不,那是我在问什么在这里:“我怀疑的特殊状态/proc/文件作为理由,但是当我的内容复制/proc/meminfo到一个普通的文件和使用,结果是一样的:”这是不是/proc/文件,读取常规文件也更快!
甜点

Answers:


47

在这里,与打开文件而不是读取变量的内容无关,而更多的是关于是否进行额外的处理。

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

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

/proc/meminfo相比之下,有关读取的部分微不足道,内核在其中生成信息的时间很少,而在grep读取信息的时间也很少。

如果您以此为基础进行运行strace -c,则会发现用于读取的一个open()和一个read()系统调用/proc/meminfo是花生,而不是其他所有grep启动操作(strace -c不计分叉)。

在:

a=$(</proc/meminfo)

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

在:

printf '%s\n' "$a" | grep '^MemFree'

Shell需要产生两个进程,它们同时运行,但通过管道相互交互。管道的创建,拆除以及对其进行写入和读取的成本很少。更大的成本是产生了额外的过程。流程的调度也有一些影响。

您可能会发现使用zsh <<<运算符会使它更快一些:

grep '^MemFree' <<< "$a"

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

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

 grep '^MemFree' << EOF
 $a
 EOF

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

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

的内容/proc/meminfo简短,不需要花费很多时间来制作。如果要节省一些CPU周期,则要删除昂贵的部分:派生进程和运行外部命令。

喜欢:

IFS= read -rd '' meminfo < /proc/meminfo
memfree=${meminfo#*MemFree:}
memfree=${memfree%%$'\n'*}
memfree=${memfree#"${memfree%%[! ]*}"}

避免bash尽管其模式匹配效率很低。使用zsh -o extendedglob,您可以将其缩短为:

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

请注意,^在许多shell中(至少是带有Extendedglob选项的Bourne,fish,rc,es和zsh),它是特殊的,我建议引用它。还要注意,echo它不能用于输出任意数据(因此我在printf上面使用过)。


4
对于printf您所说的情况,外壳程序需要产生两个进程,但是printf外壳程序不是内置的吗?
David Conrad

6
@DavidConrad是的,但是大多数外壳程序都不尝试分析管道在当前过程中可以运行哪些部分。它只是叉起来,让孩子们弄清楚。在这种情况下,父进程分叉两次;左侧的孩子然后看到一个内置的并执行它;右侧的孩子看到grep并执行。
chepner

1
@DavidConrad,管道是IPC机制,因此无论如何,双方都必须在不同的进程中运行。在中时A | B,有一些像AT&T ksh或zsh这样B的shell在当前shell进程中运行(如果它是内置命令,compound或function命令),我不知道A在当前进程中运行的shell 。如果要执行此操作,他们将必须以复杂的方式处理SIGPIPE,就像A在子进程中运行一样,并且必须终止外壳,以使行为B尽早退出时不会太令人惊讶。B在父进程中运行要容易得多。
StéphaneChazelas

Bash支持<<<
D. Ben Knoble

1
@ D.BenKnoble,我并不是在暗示bash不支持<<<,只是操作员从来到zsh$(<...)从KSH来了。
斯特凡Chazelas

6

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

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

区别在于/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

1

在这两种情况下,您都在调用外部命令(grep)。外部调用需要一个子shell。分叉该外壳是造成延迟的根本原因。两种情况都是相似的,因此:相似的延迟。

如果您只想读取一次外部文件并多次使用(通过变量),请不要离开外壳:

meminfo=$(< /dev/meminfo)    
time for i in {1..1000};do 
    [[ $meminfo =~ MemFree:\ *([0-9]*)\ *.B ]] 
    printf '%s\n' "${BASH_REMATCH[1]}"
done

只需约0.1秒,而不是grep调用的整个1秒。

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.