Почему printf в bash быстрее, чем /usr/bin/printf?

Почему printf в bash быстрее, чем /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. Исполняемый файл должен быть загружен с диска, что приводит к замедлению работы, поскольку для загрузки двоичного объекта с диска, на котором хранится исполняемый файл, необходимо обратиться к жесткому диску.
  2. Исполняемый файл обычно создается с использованием динамических библиотек, поэтому в исполняемый файл также необходимо загрузить некоторые вторичные файлы (т. е. считывать с жесткого диска дополнительные двоичные данные).
  3. Накладные расходы операционной системы. Каждый вызываемый вами процесс влечет накладные расходы в виде создания для него идентификатора процесса. Пространство в памяти также должно быть выделено для размещения двоичных данных, загружаемых с жесткого диска на шагах 1 и 2, а также для нескольких структур, которые должны быть заполнены для хранения таких вещей, как среда процессов (переменные среды и т. д.)

отрывок из следа/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

С построенной версией printfвсех библиотек, от которых он зависит, а также его двоичным блоком, которые уже были загружены в память, когда был вызван Bash. Так что ничего из этого не придется делать снова.

Фактически, когда вы вызываете встроенные «команды» Bash, вы на самом деле выполняете то, что равносильно вызову функции, поскольку все уже загружено.

Аналогия.

Если вы когда-либо работали с языком программирования, например, Perl, это эквивалентно вызовам функции ( system("mycmd")) или использованию обратных кавычек ( `mycmd`). Когда вы делаете что-либо из этого, вы разветвляете отдельный процесс с его собственными накладными расходами, вместо использования функций, которые предлагаются вам через основные функции Perl.

Анатомия управления процессами Linux

На IBM Developerworks есть довольно хорошая статья, в которой разбираются различные аспекты создания и уничтожения процессов Linux, а также различные библиотеки C, задействованные в этом процессе. Статья называется:Анатомия управления процессами Linux — создание, управление, планирование и уничтожение. Он также доступен какPDF.

решение2

Выполнение внешней команды /usr/bin/printfприводит к созданию процесса, который встроенная оболочка не создает. Таким образом, для цикла из 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

Вы можете видеть, что по крайней мере в этом отношении GNU printfне оптимизирован для производительности. Нет особого смысла оптимизировать команду типа printfв любом случае, потому что в 99,999% случаев время, потраченное на выполнение действия, в любом случае будет затмеваться временем выполнения. Гораздо разумнее оптимизировать команды типа grepили sed, которые потенциально могут обрабатывать гигабайты данных водинбегать.

Связанный контент