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쉘 내장보다 훨씬 빠른 작업을 수행하는 프로그램을 기대했을 것입니다 . 물론 내장 프로그램은 이미 메모리에 로드되어 있지만 실제 실행 시간은 전용 프로그램에서 더 빨라야겠죠? 최고의 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. 실행 파일은 디스크에서 로드되어야 하며, 실행 파일이 저장된 디스크에서 바이너리 blob을 로드하기 위해 HDD에 액세스했기 때문에 속도가 느려집니다.
  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가 호출될 때 Bash가 의존하는 모든 라이브러리와 바이너리 blob 의 빌드 버전이 printf이미 메모리에 로드되었습니다. 따라서 그 중 어느 것도 다시 발생할 필요가 없습니다.

Bash에 내장된 "명령"을 효과적으로 호출하면 모든 것이 이미 로드되었기 때문에 실제로 함수 호출에 해당하는 결과를 얻게 됩니다.

비유

system("mycmd")Perl과 같은 프로그래밍 언어로 작업한 적이 있다면 함수( )를 호출하거나 백틱( )을 사용하는 것과 같습니다 `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잠재적으로 기가바이트의 데이터를 처리할 수 있는 명령을 최적화하는 것이 훨씬 더 합리적입니다.하나달리다.

관련 정보