ter*_*don 5 performance bash shell-builtin
我有两种调用printf
系统的方法:
$ type -a printf
printf is a shell builtin
printf is /usr/bin/printf
$ file /usr/bin/printf
/usr/bin/printf: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically
linked (uses shared libs), for GNU/Linux 2.6.32,
BuildID[sha1]=d663d220e5c2a2fc57462668d84d2f72d0563c33, stripped
Run Code Online (Sandbox Code Playgroud)
因此,一个是内置的 bash,另一个是正确编译的可执行文件。我本来期望一个程序的唯一工作是printf
比内置的 shell 快得多。当然,内置函数已经加载到内存中,但在专用程序中实际执行时间应该更快,对吗?它会被优化以在最好的 Unix 哲学中很好地做一件事。
显然不是:
$ >/tmp/foo; time for i in `seq 1 3000`; do printf '%s ' "$i" >> /tmp/foo; done;
real 0m0.065s
user 0m0.036s
sys 0m0.024s
$ >/tmp/foo; time for i in `seq 1 3000`; do /usr/bin/printf '%s ' "$i" >> /tmp/foo; done;
real 0m18.097s
user 0m1.048s
sys 0m7.124s
Run Code Online (Sandbox Code Playgroud)
正如@Guru 指出的那样,其中很多是因为创建线程的成本仅由/usr/bin/printf
. 如果仅此而已,如果在循环外运行,我希望可执行文件比内置文件更快。不幸的是,/usr/bin/printf
它可以接受的变量大小有限制,所以我只能用一个相对较短的字符串来测试它:
$ i=$(seq 1 28000 | awk '{k=k$1}END{print k}'); time /usr/bin/printf '%s ' "$i" > /dev/null;
real 0m0.035s
user 0m0.004s
sys 0m0.028s
$ i=$(seq 1 28000 | awk '{k=k$1}END{print k}'); time printf '%s ' "$i" > /dev/null;
real 0m0.008s
user 0m0.008s
sys 0m0.000s
Run Code Online (Sandbox Code Playgroud)
内置程序仍然始终如一且明显更快。为了更清楚,让两者都开始新的进程:
$ time for i in `seq 1 1000`; do /usr/bin/printf '%s ' "$i" >/dev/null; done;
real 0m33.695s
user 0m0.636s
sys 0m30.628s
$ time for i in `seq 1 1000`; do bash -c "printf '%s ' $i" >/dev/null; done;
real 0m3.557s
user 0m0.380s
sys 0m0.508s
Run Code Online (Sandbox Code Playgroud)
我能想到的唯一原因是正在打印的变量是内部变量bash
,可以直接传递给内置变量。这足以解释速度的差异吗?还有哪些因素在起作用?
调用流程的部分“费用”是必须发生一些资源密集型的事情。
摘录 /usr/bin/printf
$ strace /usr/bin/printf "%s\n" "hello world"
*execve("/usr/bin/printf", ["/usr/bin/printf", "%s\\n", "hello world"], [/* 91 vars */]) = 0
brk(0) = 0xe91000
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fd155a6b000
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
open("/etc/ld.so.cache", O_RDONLY) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=242452, ...}) = 0
mmap(NULL, 242452, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fd155a2f000
close(3) = 0
open("/lib64/libc.so.6", O_RDONLY) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0p\357!\3474\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=1956608, ...}) = 0
mmap(0x34e7200000, 3781816, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x34e7200000
mprotect(0x34e7391000, 2097152, PROT_NONE) = 0
mmap(0x34e7591000, 20480, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x191000) = 0x34e7591000
mmap(0x34e7596000, 21688, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x34e7596000
close(3) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fd155a2e000
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fd155a2c000
arch_prctl(ARCH_SET_FS, 0x7fd155a2c720) = 0
mprotect(0x34e7591000, 16384, PROT_READ) = 0
mprotect(0x34e701e000, 4096, PROT_READ) = 0
munmap(0x7fd155a2f000, 242452) = 0
brk(0) = 0xe91000
brk(0xeb2000) = 0xeb2000
brk(0) = 0xeb2000
open("/usr/lib/locale/locale-archive", O_RDONLY) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=99158752, ...}) = 0
mmap(NULL, 99158752, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fd14fb9b000
close(3) = 0
fstat(1, {st_mode=S_IFIFO|0600, st_size=0, ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fd155a6a000
write(1, "hello world\n", 12hello world
) = 12
close(1) = 0
munmap(0x7fd155a6a000, 4096) = 0
close(2) = 0
exit_group(0) = ?*
Run Code Online (Sandbox Code Playgroud)
通过以上内容,您可以了解/usr/bin/printf
由于它是独立的可执行文件而必须产生的额外资源。
在printf
调用 Bash 时,它所依赖的所有库的构建版本及其二进制 blob 已经加载到内存中。所以这一切都不必再次发生。
实际上,当您向 Bash 调用内置“命令”时,您实际上是在进行函数调用,因为所有内容都已加载。
如果您曾经使用过诸如 Perl 之类的编程语言,那么它相当于调用函数 ( system("mycmd")
) 或使用反引号 ( `mycmd`
)。当你做这些事情中的任何一件时,你是在分叉一个单独的进程,它有自己的开销,而不是使用通过 Perl 的核心函数提供给你的函数。
IBM Developerworks 上有一篇很好的文章,它分解了 Linux 进程如何创建和销毁的各个方面以及进程中涉及的不同 C 库。文章标题为:Linux 进程管理剖析——创建、管理、调度和销毁。它也可以作为PDF 使用。