time 결과의 real, user, sys는 무엇이 다를까: 경과 시간과 CPU 시간

time 명령의 real, user, sys가 각각 무엇을 세는지 POSIX 정의로 정리하고, user+sys가 real보다 크거나 작을 때 읽는 법, 셸 time과 /usr/bin/time의 차이를 설명합니다.

개발운영체제리눅스

time으로 명령을 재면 real, user, sys 세 값이 나옵니다. real만 보고 느리다고 판단하거나 세 값을 그냥 더하면 잘못 읽기 쉽습니다. 각 값이 무엇을 세는지는 POSIX에 정의되어 있습니다.

time -p ./build.sh

세 값의 정의

POSIX의 time 유틸리티에서 real은 유틸리티를 시작한 때부터 끝날 때까지 흐른 실제 시간입니다. 흔히 벽시계 시간(wall-clock time)이라고 합니다. user는 사용자 CPU 시간으로 times()의 tms_utime과 tms_cutime을 더한 값이고, sys는 시스템 CPU 시간으로 tms_stime과 tms_cstime을 더한 값입니다. -p를 주면 세 값을 초 단위로 한 줄씩 출력합니다.

tms_utime은 프로세스가 자신의 명령을 실행하는 데 쓴 CPU 시간이고, tms_stime은 시스템이 그 프로세스를 대신해 일한 CPU 시간입니다. Linux의 getrusage(2) 매뉴얼은 같은 구분을 사용자 모드와 커널 모드에서 실행한 시간이라고 설명합니다. 시스템 호출을 처리하느라 커널이 쓴 시간은 sys에 잡힙니다.

자식 프로세스는 wait 이후에 더해진다

tms_cutime과 tms_cstime은 자식 프로세스 몫입니다. POSIX에 따르면 종료된 자식의 시간은 wait(), waitid(), waitpid()가 그 자식의 프로세스 ID를 돌려줄 때 부모의 값에 포함됩니다. 자식이 자기 자식을 기다리지 않았다면 손자의 시간은 들어가지 않습니다. 여러 프로세스를 띄우는 스크립트도 time으로 잴 수 있는 이유가 이것입니다. 측정 대상 유틸리티가 어떤 프로세스를 기다리지 않으면 그 프로세스의 CPU 시간은 해당 time 결과에 포함되지 않습니다.

user + sys와 real을 비교하기

Linux getrusage()의 RUSAGE_SELF는 호출한 프로세스 안 모든 스레드의 사용량을 합치지만 자식 프로세스는 포함하지 않습니다. 기다린 자식의 사용량은 RUSAGE_CHILDREN으로 따로 집계되며, time이 사용하는 자식 시간 필드에 대응합니다. 여러 CPU에서 병렬로 돈 스레드, 그리고 time에 포함된 기다린 자식 프로세스의 CPU 시간은 각각 누적되므로 user + sys가 real보다 클 수 있습니다. GNU time의 %P는 (%U + %S) / %E로 계산하기 때문에 100%를 넘기도 합니다.

반대로 real이 user + sys보다 훨씬 크면, 측정 구간 동안 평균적으로 CPU 한 개보다 적게 사용했다는 뜻입니다. 디스크나 네트워크, 타이머, CPU 스케줄링을 기다렸을 수 있지만 이 세 값만으로 원인을 가릴 수는 없습니다. 스레드나 자식 프로세스가 동시에 도는 작업에서는 real - (user + sys)를 실제 대기 시간으로 볼 수 없습니다.

셸의 time과 /usr/bin/time

bash에서 time은 예약어라서 파이프라인 전체와 셸 함수까지 잴 수 있고, 출력 형식은 TIMEFORMAT 변수로 바꿉니다. GNU time이 /usr/bin/time에 설치된 시스템에서는 그 경로로 셸 예약어 대신 별도 프로그램을 실행할 수 있으며, /usr/bin/time --version으로 구현을 확인할 수 있습니다. GNU time은 -v나 형식 지정자로 메이저·마이너 페이지 폴트(%F, %R)와 자발적·비자발적 컨텍스트 스위치(%w, %c)도 보여 줍니다. 매뉴얼에는 시스템에 따라 일부 값이 0으로 나올 수 있다고 적혀 있고, Linux도 관리하지 않는 rusage 필드는 0으로 채웁니다.

표준과 Linux의 경계

POSIX가 struct rusage에 반드시 두라고 정한 멤버는 ru_utime과 ru_stime 둘뿐입니다. 페이지 폴트나 컨텍스트 스위치 횟수는 Linux 같은 구현이 따로 채우는 값이라 다른 운영체제에서는 그 시스템의 문서를 확인해야 합니다. 프로그램 안에서 잴 때는 Linux clock_gettime(2)의 CLOCK_PROCESS_CPUTIME_ID로 프로세스의 CPU 시간을, CLOCK_MONOTONIC으로 단조 증가하는 경과 시간을 잴 수 있습니다. Linux의 CLOCK_MONOTONIC은 시스템이 일시 중단된 시간을 세지 않으므로 그 시간까지 포함해야 한다면 CLOCK_BOOTTIME을 써야 합니다. CLOCK_REALTIME은 시스템 시각이 바뀔 때 값이 튈 수 있어 기간 측정에는 일반적으로 맞지 않습니다.

시스템이 느린데 CPU 시간이 적다면 Linux에서 열린 파일과 디스크 공간처럼 CPU 밖에서 생기는 문제도 함께 살펴봐야 합니다. 시스템 시각과 Unix time은 유닉스 시간 설명에서 이어서 볼 수 있습니다.

블로그로