[MPC/CUDA] Elapsed Time(시간 측정)

우리는 두 가지 버전의 프로그램을 실행시키는데, 하나는 CPU 기반의 순차 처리(CPU-based sequential execution), 또 다른 하나는 CUDA 기반의 병렬 처리(CUDA-based parallel execution)이다. 

이 둘의 실행 시간을 비교할 수 있는 방법에는 어떤 것이 있을까?

이때 elapsed time을 사용하여 비교하는데, 실행 시간을 측정하는 방법은 다음과 같다. 

  1. 현재 시간을 기록할 수 있는 방법을 통해 시작 시간을 설정 / start time = current time
  2. job 실행
  3. 실행이 끝나는 시간을 설정 / end time = current time
  4. 이 둘의 차이(Elapsed time = end time - start time)가 총 실행된 시간을 의미한다. 

이 방법에 대해 자세하게 알아보자.


📂 Wall-clock-time VS CPU time

Wall-clock-time(=elapsed real time)

컴퓨터 프로그램이 실행되면서 실제로 흘러간 시간을 나타낸다. 이론상으로 벽시계(wall-clock)로 측정해도 같은 결과를 얻을 수 있다 하여 붙여진 이름이다. 

CPU time(CUDA에서는 GPU time)

컴퓨터 프로그램이 실행 중에 CPU(GPU)를 사용한 시간을 나타낸다. 보통, system time(kernel time)이라는 운영체제에서 사용한 시간과 user time(user cpu time)이라는 사용자 영역에서 사용한 시간을 더하여 표현된다. 

 

일반적으로, time sharing 방식이 동작하는 등 ... 어떠한 특별한 내부적인 동작 중에는 CPU를 사용하지 않기 때문에 

CPU time < Wall-clock-time 이 된다. 

하지만, 병렬 프로그래밍에서는 이론적으로 여러 CPU들이 동시에 실행되므로 그 양이 많아지게 되면, CPU time > Wall-clock-time인 상황도 발생할 수 있다. 

📂 Chrono

chronograph라고 하여, 스톱워치 기능이 있는 시계를 의미한다. C++에서는 이 라이브러리를 사용하여 시간을 측정할 수 있고 우리는 이를 사용하게 될 것이다.

#include <chrono>
...
using namespace std::chrono;

위와 같이 먼저 선언을 해주고 사용하자. 

chrono는 nano-second까지 측정이 가능하고, wall-clock-time을 측정한 결과를 반환한다!

<chrono> 헤더 파일을 살펴보면, hours부터 minutes ... nanoseconds까지의 시간 단위를 반환할 수 있도록 되어있다.

in <chrono>

  예제 코드를 살펴보자.

#include <stdio.h>
#include <time.h>
#include <chrono>
using namespace std;
using namespace std::chrono;

void bigjob(void){
...
}

int main(void){
    system_clock::time_point chrono_begin = system_clock::now();
    //work
    bigjob();
    system_clock::time_point chrono_end = system_clock::now();
	
    //calculation
    microseconds elapsed_usec = duration_cast<microseconds>(chrono_end-chrono_begin);
    
    printf("elapsed time = %ld usec", (long)elapsed_usec.count());
    
    return 0;
}

system_clock::time_point는 시간을 저장하는 데이터 타입이다. 그리고 뒤에 따라오는 system_clock::now()현재 시간을 반환해준다.

elapsed_usec를 구하는 부분에서 앞의 microseconds는 chrono에 들어 있는 mircoseconds를 의미하고, duration_cast <microseconds>는 뒤의 계산 결괏값을 micro로 단위를 변환해주는 역할을 한다.

결과값 출력 부분의 elapsed_usec.count()에서 .count()는 해당 변수에 대응되는 정수를 반환하는 역할을 한다. 

메크로 만들기

위의 시간 측정 방법을 매번 작성하기 귀찮으니 메크로로 만들어 필요할 때마다 불러올 수 있도록 작성해보자. 작성한 메크로를 "common.cpp" 파일 안에 저장하고 이를 프로그램을 작성할 때마다 include 할 것이다. 

 

chrono::system_clock::time_point __time_begin[8] = {chrono::system_clock::now(), };

#define ELAPSED_TIME_BEGIN(N) do{\
    __time_begin[(N)] = chrono::system_clock::now();\
    printf("elapsed wall-clock-time[%d] started\n",(N));\
    fflush(stdout);\
    } while(0)

#define ELAPSED_TIME_END(N) do{\
    chrono::system_clock::time_point time_end = chrono::system_clock::now();\
    chrono::microseconds elapsed_msec = \
    	chrono::duration_cast<chrono::microseconds>(time_end - __time_begin[(N)]);\
    printf("elapsed wall-clock-time[%d] = %ld usec\n", (N), (long)elapsed_msec.count());\
    fflush(stdout);\
    }while(0)

시작 시간을 배열로 저장한 이유는, 여러 함수들에 대해 스탑워치를 실행할 수 있도록 하기 위함이다. 

이를 사용한 예제를 간단히 작성하면 다음과 같다.

#include "./common.cpp"
...
int main(void){
    ELAPSED_TIME_BEGIN(0);
    //work
    bigjob():
    ELAPSED_TIME_END(0);
    
    return 0;
}

주의할 점은 chronowall-clock-time을 측정하기 위한 라이브러리란 것이다.

그렇다면, CPU time은 어떻게 측정할까? 바로 알아보자.

📂 clock() function

clock() 함수는 clock_t clock(void); 형태이고 <time.h> 헤더 안에 들어있다. processor time(CPU/GPU time)을 측정하는데 사용된다. time.h에 미리 정의되어 있는 CLOCKS_PER_SEC로 나누어 값을 얻을 수 있는데, 간략하게 보면 아래와 같다.

float clock_sec = (float)clock()/CLOCKS_PER_SEC;
float clock_usec = (float)clock()*1000000/CLOCKS_PER_SEC;

예제를 살펴보자.

#include <stdio.h>
#include <time.h>
#include <chrono>
using namespace std;
using namespace std::chrono;

void bigjob(void){
...
}

int main(void){
    system_clock::time_point chrono_begin = system_clock::now();
    clock_t clock_begin = clock();
    //work
    bigjob();
    system_clock::time_point chrono_end = system_clock::now();
    clock_t clock_end = clock();
	
    //calculation
    microseconds elapsed_usec = duration_cast<microseconds>(chrono_end-chrono_begin);
    printf("elapsed time = %ld usec", (long)elapsed_usec.count());
    
    long clock_elapsed_usec = (long)(clock_end-clock_begin)*1000000/CLOCKS_PER_SEC;
    printf("elapsed CPU time = %ld usec\n", clock_elapsed_usec);
    
    return 0;
}

기존의 chrono를 통해 wall-clock-time을 구한 코드에 clock()을 이용하여 CPU time을 구하는 코드를 추가한 것이다. 결과는 다음과 같다.

결과 창에서 볼 수 있듯, time-sharing 등의 이유로 wall-clock-time보다 CPU-time이 적게 소요되는 것을 확인할 수 있다.

(swap in/out 같은 것은 CPU를 사용하지 않기 때문에 CPU time에 포함되지 않음!!)

📂 sleep() function

thread를 잠시 멈추는 역할을 하는 함수이다. 주의할 점은 wall-clock-time 기준이라는 것이다. UNIX 기반과 Windows 기반에서의 사용법이 조금 다른데 다음을 보자.

Unix/Linux

  • #include <unistd.h>
  • unsigned int sleep(unsigned int seconds); // 초 단위로 지정
  • int usleep(useconds_t usec); // µsec 단위로 지정

Windows

  • #include <windows.h>
  • void Sleep(DWORD dwMilliseconds); // msec 단위로 지정

위에서 확인할 수 있듯, 3가지 함수가 받은 인자의 시간 단위가 모두 다르다는 것에 주의할 필요가 있다.

 

sleep()을 이용한 한 가지 예제를 더 살펴보자.

...
#if defined(__linux__)
    usleep(100*100);
#else
    Sleep(100);//100msec
#endif

microseconds chrono_elapsed_usec
	=duration_cast<microseconds>(chrono_end - chrono_begin);
printf("elapsed time = %ld usec\n", (long)chrono_elapsed_usec.count());

long clock_elapsed_usec = (long)(clock_end - clock_begin)*1000000/CLOCKS_PER_SEC;
printf("elapsed CPU time = %ld usec\n", clock_elapsed_usec);
...

linux환경일 경우 usleep(), 그 외의 환경에서는 Sleep()을 실행하는 코드이다. 결과를 확인해보자.

chrono로 측정한 elapsed time이 CPU time에 비해 매우 큰 것을 확인할 수 있다. 그 이유는,

chrono는 wall-clock-time을 측정하기 때문에 sleep 한 100000 µsec도 포함시켜 시간을 반환하지만, clock()은 CPU time을 측정하기 때문에 CPU가 동작한 시간만을 측정 즉, sleep 하는 동안에는 CPU를 사용하지 않으므로 온전한 CPU time만을 반환하는 것이다. 

📂 Variable Arguments

C/C++ main() 함수에서 arg를 받는 방법은 다음과 같다.

int main(int argc, char* argv[], char* envp[]){...}

argc, argv는 우리에게 이미 익숙하지만, char* envp[]는 무엇을 의미할까?

envp[]는 환경변수를 말한다. 

환경변수(environment variables)란, shell에서 관리하는 특별한 variables로 일종의 global 변수로써 모든 process가 자동으로 상속받는다. 예를 들어, PATH(실행 파일을 검색하는 경로), HOME(home directory), USER(user login name)... 등이 존재한다.

 

그림으로 한 번 살펴보자.

ls -l 이 실행되는 과정을 살펴봤을 때, argv[2]에 저장되는 (char*) 0은 argv의 끝을 알리는 것으로 생략이 가능하다. 하지만, envp[] 쪽을 살펴보자. 

이는 OS가 별도로 관리하는 환경 변수로, OS가 envp배열에 모든 환경 변수들을 넣어 해당하는 main() 함수의 char* envp[] 파라미터로 넘겨준다. 마지막의 envp [k]=(char*) 0;는 argv와 다르게 꼭 넣어줘야 하는데, 그 이유는 환경 변수의 정확한 개수를 모르기 때문에 항상 envp배열의 끝을 명시해주기 위함이다. 

'대규모병렬컴퓨팅(MPC)' 카테고리의 다른 글

[MPC/CUDA] Vector Addition  (0) 2022.10.21
[MPC/CUDA] CUDA kernel Launch  (1) 2022.10.21
[MPC/CUDA] Error Check  (0) 2022.10.13
[MPC/CUDA] CUDA Kernel  (0) 2022.10.13
[MPC/CUDA] CPU Kernel  (0) 2022.10.13