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쉘 내장 기능보다 훨씬 더 빠른 작업을 수행하는 프로그램을 기대했을 것입니다 . 물론 내장된 함수는 이미 메모리에 로딩되어 있지만 실제 실행시간은 전용 프로그램에서 더 빨라야겠죠? 최고의 유닉스 철학에서 한 가지 일을 아주 잘 수행하도록 최적화될 것입니다.

당연히 아니:

$ >/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가 의존하는 모든 라이브러리의 빌드된 버전 printf과 해당 바이너리 blob이 메모리에 로드됩니다. 따라서 이런 일이 다시는 일어나지 않아야 합니다.

실제로 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하나달리기.

관련 정보