У меня есть два способа вызова 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, а также для нескольких структур, которые должны быть заполнены для хранения таких вещей, как среда процессов (переменные среды и т. д.)
отрывок из следа/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
, которые потенциально могут обрабатывать гигабайты данных водинбегать.