为什么 bash 的 printf 比 /usr/bin/printf 快?

为什么 bash 的 printf 比 /usr/bin/printf 快?

我有两种方式调用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

因此,一个是 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

正如 @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

内置功能仍然始终如一且明显更快。为了更清楚地说明这一点,让两个进程都启动新的进程:

$ 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

我能想到的唯一原因是打印的变量是内置变量的内部变量bash,可以直接传递给内置变量。这足以解释速度的差异吗?还有哪些其他因素在起作用?

答案1

独立的 printf

调用进程的部分“费用”是必须发生一些资源密集型的事情。

  1. 可执行文件必须从磁盘加载,这会导致速度变慢,因为必须访问 HDD 才能从存储可执行文件的磁盘加载二进制 blob。
  2. 可执行文件通常是使用动态库构建的,因此还必须加载可执行文件的一些辅助文件(即从 HDD 读取更多二进制 blob 数据)。
  3. 操作系统开销。您调用的每个进程都会产生开销,其形式是必须为其创建进程 ID。内存中的空间也将被划分出来,以容纳在步骤 1 和 2 中从 HDD 加载的二进制数据,以及必须填充的多个结构来存储进程的环境(环境变量等)等内容。

摘录一段/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)                           = ?*

通过上面的内容,您可以了解/usr/bin/printf由于它是独立的可执行文件而必须产生的额外资源。

内置 printf

当调用 Bash 时,它所依赖的所有库的构建版本printf及其二进制 blob 都已加载到内存中。因此,这些都不必再次发生。

实际上,当您调用 Bash 的内置“命令”时,您实际上是在进行函数调用,因为所有内容都已加载。

打个比方

如果您曾经使用过编程语言,例如 Perl,那么这相当于调用函数 ( system("mycmd")) 或使用反引号 ( `mycmd`)。当您执行上述任一操作时,您将分叉一个单独的进程,并有其自身的开销,而不是使用通过 Perl 核心函数提供给您的函数。

Linux 进程管理剖析

IBM Developerworks 上有一篇非常好的文章,详细介绍了 Linux 进程如何创建和销毁的各个方面,以及该进程中涉及的不同 C 库。文章标题为:Linux进程管理剖析——创建、管理、调度和销毁。它也可以作为PDF

答案2

执行外部命令/usr/bin/printf会导致创建进程,而内置 shell 则不会。因此,对于 3000 个循环,创建了 3000 个进程,因此速度较慢。

您可以通过在循环外运行它们来检查这一点:

答案3

虽然生成和设置新进程以及加载、执行和初始化、清理和终止程序及其库依赖项的时间远远掩盖了执行操作所需的实际时间,但这里已经涵盖了这一事实printf对于一项昂贵的行动,有不同的实施时间,即不是被其他的掩盖了:

$ time /usr/bin/printf %2000000000s > /dev/null
/usr/bin/printf %2000000000s > /dev/null  13.72s user 1.42s system 99% cpu 15.238 total

$ time busybox printf %2000000000s > /dev/null
busybox printf %2000000000s > /dev/null  1.50s user 0.49s system 95% cpu 2.078 total


$ time bash -c 'printf %2000000000s' > /dev/null
bash -c 'printf %2000000000s' > /dev/null  4.59s user 3.35s system 84% cpu 9.375 total

$ time zsh -c 'printf %2000000000s' > /dev/null
zsh -c 'printf %2000000000s' > /dev/null  1.48s user 0.24s system 81% cpu 2.115 total

$ time ksh -c 'printf %2000000000s' > /dev/null
ksh -c 'printf %2000000000s' > /dev/null  0.48s user 0.00s system 88% cpu 0.543 total

$ time mksh -c 'printf %2000000000s' > /dev/null
mksh -c 'printf %2000000000s' > /dev/null  13.59s user 1.57s system 99% cpu 15.262 total

$ time ash -c 'printf %2000000000s' > /dev/null
ash -c 'printf %2000000000s' > /dev/null  13.74s user 1.42s system 99% cpu 15.214 total

$ time yash -c 'printf %2000000000s' > /dev/null
yash -c 'printf %2000000000s' > /dev/null  13.73s user 1.40s system 99% cpu 15.186 total

可以看到,至少在这方面,GNUprintf并没有针对性能进行优化。无论如何,优化命令没有多大意义,printf因为对于 99.999% 的使用情况,执行操作所花费的时间无论如何都会被执行时间所掩盖。优化诸如grep或之类的命令更有意义,sed这些命令可能会处理千兆字节的数据跑步。

相关内容