Modern C++

[C++] chrono vs ctime

dev-ohdam 2024. 11. 26. 15:44

 서버 프로그래밍을 수행할 때, 시간과 관련된 요소를 자주 사용하기 마련이다. 예를 들면, 서버에서 게임 업데이트 로직을 함께 굴리거나, 송수신 딜레이를 파악해 수용 가능 인원을 넘어선 연결을 차단하는 등이 있다.

 

 과거, C/C++언어 프로그래밍을 배웠던 사람들은 <ctime>을 활용해 시간과 관련된 다양한 조작을 수행했다. 하지만

<ctime> 그렇게 잘 디자인된 라이브러리는 아니다. 정확도가 1초 단위가 한계이기 때문이다. 사람을 기준으로 1초는 매우 짧은 시간이지만, 1초에도 수만 번의 연산을 수행하는 컴퓨터 입장에선 아주 느린 단위이다. 이는 컴퓨터에서 수행하는 짧은 시간 단위의 연산 간의 정확한 시간 측정을 위해 활용하기 어렵다. 하물며 요즘은 그래픽 렌더링도 1/120초 단위로 이루어지는 게 태반이다.

 

 

 이를 개선한 버전을 C++11부터 <chrono>라는 새로운 표준 라이브러리를 통해 제공된다.

<ctime>보다 정확한 시간 측정이 가능하며, 조금 더 복잡한 문법을 가진다는 것이 특징이다.

ctime의 시간 함수 time()과 clock()

 ctime의 대표적인 시간 측정 함수는 time이다. 이는 time_t라는 구조체의 주소를 인자로 받으며, 

1970년 1월 1일 기준으로 경과한 시간 값을 time_t 구조체에 받는다. 이때 반환받은 시간값은 정수로 표현된다.

해당 함수의 장점은 시간 경과 처리 단위를 몇십년 단위까지 적용할 수 있다는 점이다. 하지만 단점으로는 정확도가

초 단위라는 것이다.

 

 또 다른 시간 측정 함수는 clock()이다. 이 함수는 clock_t라는 구조체를 반환한다.  time_t와 마찬가지로 clock_t 또한 정수로 표현된다. clock()은 time()과 다르게, 프로그램이 시작한 시점을 기준으로 경과한 시간을 측정하며, time() 보다 비교적 정확도가 높다. (거의 밀리초) 하지만 측정된 시간에 대한 변환이 최대 분까지 밖에 적용할 수 없다는 단점이 있다.

 

 

C++11에서 등장한 시간 표준 라이브러리 chrono

 chrono는 이러한 기존 시간 관련 함수의 단점을 보완하여 등장했다.

chrono는 크게 3 가지 메 컨셉을 기반으로 구현됐다.

  1.  시계 (Clock): chrono에서 제공되는 시계는 시작 날짜(***Epoch***)와 틱 주기(Tick Rate)를 기반으로 동작한다.
    Epoch: 1970년 1월 1일을 기준으로 한다.
    Tick-Rate: 초 당 몇 개의 Tick을 발생시키는지에 관한 값이다. 기본 단위는 1초 당 1 Tick이며,
    이 초를 기준으로 한 분수 형태(Fraction)로 표현된다.
  2.  타임 포인트(Time-Point): Epoch 기준으로 경과된 Tick 값으로, Clock을 통해 얻을 수 있다.
  3.  간격(Duration):  두 타임 포인트 간의 경과 시간을 의미한다.

 

Duration-Type (feat. literal-surffix)

 chrono는 여러 단위의 시간을 표현할 수 있다. 기본적인 시간 표현부터 시작해서 C++20에선 날짜 표현까지 지원한다.

시간은 최소 나노 초 단위까지 표현 가능하다.

 

 Duration-Type은 이러한 단위에 맞는 시간값을 정수 형태로 표현한 타입이다.

std::chrono::hours, std::chrono::seconds 등으로 표현할 수 있다. 그리고 이 모든 시간 단위를 통합적으로 표현할 수 있는 타입이 std::chrono::duration이 있다.

 

그뿐만 아니라, C++14부터는 이에 관한 Literal을 제공하기 때문에, 좀 더 가독성 높은 코드를 작성할 수 있다.  

 

이때 주의해야 할 점은, Duration-Type은 operator << 를 지원하지 않기 때문에 std::cout을 활용해 직접 출력하지 못한다. 

그 대신 Duration-Type::count() 멤버 함수를 활용해 정수값을 받아 출력할 수 있다. 출력 목적 외에 count() 함수는 잘 사용하지 않는다. 또 다른 주의점으로는, 정수 타입을 시간 타입으로 형변환 하는 것이 불가능하다는 점이다.

void func(std::chrono::seconds sec)
{
	std::cout << "not-for integer argument\n.";
}

int main()
{
 std::chrono::hours hour = 5; // 5 hour
 std::chrono::minutes min = 30; // 30 min
 auto hour_with_literal = 5h;
 auto minutes_with_literal = 30min;

auto ms_with_literal = 43ms;
 auto sec_with_literal = 5s;

 std::cout << hour.count() << "\n" // print 5
 std::cout << (ms_with_literal + sec_with_literal).count() << "\n"; // print 5043ms
 func(5); //  not ok *** compile error
 func(5s); // ok

}

 

Duration-Type Conversion

 각 시간 단위 간의 형변환 기능 또한 제공한다. 여타 다른 Built-In 타입들과 마찬가지로 암묵적 형변환과 명시적 형변환

두 가지가 존재한다.

 

 암묵적 형변환의 경우 Data-Loss가 발생하지 않는 경우에만 가능하다. 

예를 들어  5043ms을 seconds 단위로 변경하게 될 경우, 43ms 만큼의 Data-Loss가 발생한다.

이런 경우는 암묵적 형변환을 수행할 수 없다. 반면, 분 단위를 초 단위로 형변환 하는 것은 가능하다.

(1 min -> 60s, no data loss)

 

 하지만 암묵적 형변환이 되지 않는다고 하더라도 명시적 형변환을 수행하면, Data-Loss 발생 여부와 관계없이 형변환이 가능하다. 명시적 형변환을 수행하기 위해선, std::chrono::duration_cast()라는 타입 캐스팅 함수를 사용해야 한다.

 

std::chrono::duration_cast <Duration-Type>(std::chrono::duration TimeValue)

사용법은 간단하다. 변환할 시간 값이나 time_point를 인자로 넘기고, 변환하고자 하는 타입을 탬플릿 인자로 넘겨주면

된다. 그러면 해당 함수가 시간 단위 간의 타입캐스팅을 수행한 값을 std::chrono::duration 타입으로 반환한다.

 

 std::chrono::duration()를(타입 아님. 함수임.) 사용해 정수가 아닌 다른 Built-In type으로 변환할 수도 있다.

auto t = std::chrono::duration_cast<std::chrono::seconds>(5043ms);
std::cout << t.count() << "\n"; // print 5

auto t_to_float = std::chrono::duration<float, std::chrono::seconds>(t);
std::cout << t_to_float << "\n"; // print 5.f

 

Chrono Clock

 모든 clock들은 기본적으로 하드웨어의 성능에 영향을 받지 않는다. 이에 관한 원리는 직접 찾아보는 것이 좋을 것 같다.

 

chrono 라이브러리에서 제공하는 Clock-Object는 다음 세 가지가 있다.

  1. system_clock
  2. steady_clock
  3. high_resoultion_clock: 컴파일러에 따라 system_clock 혹은 steady_clock 둘 중 하나를 의미한다.
    (window는 steady_clock)

system_clock

 system_clock은 하드웨어와 OS에 의해 관리되는 시스템 시간인 Wall-Time을 기반으로 동작한다.

C 라이브러리에서 제공하던 Clock과 유사하게 동작한다는 특징을 가진다. 아주 쉽게 설명하자면, 우리들의 컴퓨터

아래에 표시되는 시계, 캘린더 UI에 나타나는 시간들이 바로 system_clock에서 결정된 시간인 것이다.

 

 system_clock은 방금 언급한 시계 혹은 캘린더와 같은 Interative-Use에 유용하게 활용될 수 있다.

실세계의 시간 흐름과 동일하게 동기화되기 때문이다.

 

 하지만 정확한 시간 측정에는 도움이 되지 않는다. 그 이유는 system_clock은 시간의 변동에 따라 영향을 받을 수 있기

때문이다. 시간을 역행하거나 점프하는 것이 이에 해당한다.

 

  예를 들어 윤초(Leaf-Second), 혹은 서머타임(Daylight-Saving Time)이 시간 변동 요소에 해당한다.

 

steady_clock

 steady_clock은 시간 측정에 아주 특화된 시계라고 볼 수 있다. steady_clock은 오직 순행만 가능하며, 과거로 역행하는 것이 불가능하다. 그리고 실세계의 시간과 관계없이, 오직 자기 자신의 내부 스톱워치를 기반으로 일정한 주기로 흘러간다.

(Monotonic 하게 동작한다.) 보통 우리가 프로그래밍을 통해 Timer를 구현한다고 하면 이 steady_clock을 사용한다고 보면 된다.

 

std::chrono::clock-type::now()와 std::chrono::time_point

 

 Time-Point란 chrono 라이브러리에서 특정 시점을 표현하는 객체를 의미한다.

이는 std::steady_clock::now() 혹은 std::system_clock::now() 함수를 통해 얻을 수 있는데, 이는 함수 호출 기준으로, Epoch로부터 경과한 시간을 반환하는 함수들이다.

 

 Time-Point 간의 뺄셈을 통해 얻은 Duration을 바탕으로 now() 호출 시점 간의 경과 시간을 구하는 것도 가능하다.

auto t1 = std::steady_clock::now();
// .. do something
auto t2 = std::steady_clock::now();
auto elapsed_time = std::chrono::duration_cast<std::chrono::miliseconds>(t2 - t1).count();

std::cout << elapsed_time << "ms\n";

 

 

std::chrono::clock-type::now() vs std::chrono::time_point::time_since_epoch()

 마지막으로 다룰 내용은 time_point::time_since_epoch()이다.

필자의 경우 now()와 이 함수 간의 차이점을 잘 이해하지 못했는데, 그 이유는 당연하다. 둘은 똑같은 의미기 때문이다.

두 함수 모두, Epoch 기준으로 경과한 시점값을 반환한다.

 

다만 now()를 호출 시, time_point 객체를 얻을 수 있으며, 그 time_point 객체의 time_since_epoch()를 호출하면,  해당 time_point의 std::chrono::duration 값을 얻을 수 있다.

 

 std::chrono::duration_cast()를 활용할 땐, time_point 객체를 직접 인자로 넘겨줄 수 없다. 그렇기 때문에 보편적으로는 다른 time_point 간의 연산을 수행한 후, Duration으로 변환하여 넘겨주어야 한다. 해당 함수를 활용하면 바로 Duration 값을 넘길 수 있다.

 

auto tp = std::chrono::steady_clock::now();

std::duration_cast<std::chrono::seconds>(tp); // not ok compile-error
std::duration_cast<std::chrono::seconds>(tp.time_since_epoch()); // ok