-----------------
title : 씹어먹는 C++ - <17 - 3. 난수 생성(<random>)과 시간 관련 라이브러리(<chrono>) 소개>
cat_title : 17 - 3. 난수 생성(<random>)과 시간 관련 라이브러리(<chrono>) 소개
path : /C++
publish_date : 2020-1-6
tex_title : 난수 생성(<random>)과 시간 관련 라이브러리(<chrono>) 소개
next_page : 306
-----------------

이번 강좌에서는

* `<random>` 라이브러리를 활용한 난수 생성
* `<chrono>` 라이브러리를 활용한 시간 측정

에 대해서 알아봅니다.

![](/img/ChewingCpplogo.png)

안녕하세요 여러분! 이번 강좌에서는 C++ 11 에 추가된 난수(Random number)를 쉽게 생성할 수 있도록 도와주는 `<random>` 라이브러리와 시간 관련 데이터를 다룰 수 있게 도와주는 `<chrono>` 라이브러리를 살펴보도록 하겠습니다.

아무래도 C 언어를 먼저 접한 분들은 C++ 에서도 난수 생성이나 날짜 관련 계산을 위해 C 라이브러리 (`time.h` 이나 `stdlib.h`) 를 사용하는 경우가 종종 있는데 이번 기회에 왜 C++ 라이브러리를 사용해야만 하는지 짚고 넘어가도록 할 것입니다.

### C 스타일의 난수 생성의 문제점

아래는 C 스타일로 0 부터 99 까지의 난수를 생성하는 코드 입니다.

```cpp-formatted
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main() {
  srand(time(NULL));

  for (int i = 0; i < 5; i++) {
    printf("난수 : %d \n", rand() % 100);
  }
  return 0;
}
```

성공적으로 컴파일 하였다면

```exec
난수 : 75
난수 : 95
난수 : 20
난수 : 47
난수 : 41
```

와 같이 나옵니다. 참고로 위 코드는 엄밀히 말하자면 진짜 난수를 생성하는 것이 아니라 **마치 난수 처럼 보이는** 의사 난수 (pseudo random number) 을 생성하는 코드 입니다. 컴퓨터 상에서 완전한 무작위로 난수를 생성하는 것은 생각보다 어렵습니다. 그 대신에, 첫 번째 수 만 무작위로 정하고, 나머지 수들은 그 수를 기반으로 여러가지 수학적 방법을 통해서 마치 **난수 처럼 보이지만 실제로는 무작위로 생성된 것이 아닌** 수열들을 만들어내게 됩니다.

무작위로 정해진 첫 번째 수를 시드(seed) 라고 부르는데, C 의 경우 `srand` 를 통해 `seed` 를 설정할 수 있습니다. 우리의 경우 `time(NULL)` 을 통해 프로그램을 실행했던 초를 시드값으로 지정하였습니다. 그리고 `rand()` 는 호출 할 때 마다 시드값을 기반으로 무작위 처럼 보이는 수열을 생성하게 되죠.

하지만 위 코드는 여러가지 문제점들이 있습니다.

#### 시드값이 너무 천천히 변한다.

보시다시피 시드값으로 현재의 *초* 를 지정하였습니다. 이 말은 즉슨 같은 시간대에 시작된 프로그램의 경우 모두 같은 의사 난수 수열을 생성한다는 점입니다. 만일 여러가지 프로그램들이 같이 돌아가는 시스템에서 위 코드를 사용하였다면 아마 같은 난수열을 생성하는 프로그램이 생기게 될 것입니다.

#### 0 부터 99 까지 균등하게 난수를 생성하지 않는다.

위 코드에서 우리가 0 부터 99 까지 난수를 생성하기 위해서

```cpp-formatted
printf("난수 : %d \n", rand() % 100);
```

와 같이 하였습니다. 문제는 `rand()` 가 리턴하는 값이 0 부터 `RAND_MAX` 라는 점입니다. 물론 `rand()` 가 0 부터 `RAND_MAX` 까지의 모든 값들을 같은 확률로 난수를 생성하지만, 100 으로 나눈 나머지는 꼭 그러라는 법이 없습니다. 예를 들어서 `RAND_MAX` 가 128 이라고 합시다. 그렇다면 1 의 경우 `rand()` 가 리턴한 값이 1 이거나 101 일 때 생성되지만 50 의 경우 `rand()` 가 리턴한 값이 50 일 때만 생성됩니다. 따라서 1 이 뽑힐 확률이 50 이 뽑힐 확률 보다 2 배나 높게 됩니다.

#### rand() 자체도 별로 뛰어나지 않다.

무엇보다도 C 의 `rand()` 함수는 [선형 합동 생성기 (Linear congruential generator)](https://ko.wikipedia.org/wiki/%EC%84%A0%ED%98%95_%ED%95%A9%EB%8F%99_%EC%83%9D%EC%84%B1%EA%B8%B0) 이라는 알고리즘을 바탕으로 구현되어 있는데 이 알고리즘은 썩 좋은 품질의 난수열을 생성하지 못합니다. 더 깊게 설명하자면 생성되는 난수열들의 상관 관계가 높아서 일부 시뮬레이션에 사용하기에 적합하지 않습니다.

결론적으로 말하자면

```lec-warning
C++ 에서는 C 의 srand 와 rand 는 갖다 버리자!
```

### <random>

먼저 위 코드 처럼 0 부터 99 까지의 난수를 생성하는 코드를 C++ 의 `<random>` 라이브러리를 사용해서 어떻게 작성하는지 살펴보도록 하겠습니다.

```cpp-formatted
#include <iostream>
#include <random>

int main() {
  // 시드값을 얻기 위한 random_device 생성.
  std::random_device rd;

  // random_device 를 통해 난수 생성 엔진을 초기화 한다.
  std::mt19937 gen(rd());

  // 0 부터 99 까지 균등하게 나타나는 난수열을 생성하기 위해 균등 분포 정의.
  std::uniform_int_distribution<int> dis(0, 99);

  for (int i = 0; i < 5; i++) {
    std::cout << "난수 : " << dis(gen) << std::endl;
  }
}
```

성공적으로 컴파일 하였다면

```exec
난수 : 77
난수 : 11
난수 : 45
난수 : 72
난수 : 3
```

자 그렇다면 위 코드를 하나 하나씩 살펴보도록 하겠습니다.

```cpp-formatted
// 시드값을 얻기 위한 random_device 생성.
std::random_device rd;
```

앞서 C 의 경우 `time(NULL)` 을 통해서 시드값을 지정하였지만 이는 여러가지 문제점이 있었습니다. C++ 에서는 좀더 양질의 시드값을 얻기 위해 `random_device` 라는 것을 제공합니다.

대부분의 운영체제에는 진짜 난수값들을 얻어낼 수 있는 여러가지 방식들을 제공하고 있습니다. 예를 들어서 리눅스의 경우 `/dev/random` 나 `/dev/urandom` 을 통해서 난수값을 얻을 수 있습니다. 이 난수값은, 이전에 우리가 이야기 하였던 무슨 수학적 알고리즘을 통해 생성되는 가짜 난수가 아니라 정말로 컴퓨터가 실행하면서 마주치는 무작위적인 요소들 (예를 들어 장치 드라이버들의 noise) 을 기반으로한 진정한 난수를 제공합니다.

`random_device` 를 이용하면 운영체제 단에서 제공하는 *진짜* 난수를 사용할 수 있습니다. 다만 진짜 난수의 경우 컴퓨터가 주변의 환경과 무작위적으로 상호작용하면서 만들어지는 것이기 때문에 의사 난수보다 난수를 생성하는 속도가 매우 느립니다. 따라서 시드값처럼 난수 엔진을 초기화 하는데 사용하고, 그 이후의 난수열은 난수 엔진으로 생성하는 것이 적합합니다.

```cpp-formatted
// random_device 를 통해 난수 생성 엔진을 초기화 한다.
std::mt19937 gen(rd());
```

위와 같이 생성한 `random_device` 객체를 이용해서 난수 생성 엔진 객체를 정의할 수 있습니다. 만약에 `random_device` 대신에 그냥 여러분이 원하는 값을 시드값으로 넣어주고 싶다면 그냥

```cpp-formatted
std::mt19937 gen(1234);
```

와 같이 해도 됩니다.

`std::mt19937` 는 C++ `<random>` 라이브러리에서 제공하는 난수 생성 엔진 중 하나로, [메르센 트위스터](https://ko.wikipedia.org/wiki/%EB%A9%94%EB%A5%B4%EC%84%BC_%ED%8A%B8%EC%9C%84%EC%8A%A4%ED%84%B0) 라는 알고리즘을 사용합니다. 이 알고리즘은 기존에 `rand` 가 사용하였던 선형 합동 방식 보다 좀 더 양질의 난수열을 생성한다고 알려져있습니다. 무엇보다도 생성되는 난수들 간의 상관관계가 매우 작기 때문에 여러 시뮬레이션에서 사용할 수 있습니다.

참고적으로 `<random>` 라이브러리에는 위 메르센 트위스터 기반 엔진 말고도 기존의 `rand` 와 같이 선형 합동 알고리즘을 사용한 `minstd_rand` 외 여러가지 엔진들이 정의되어 있습니다. 물론 `mt19937` 이 훌륭한 난수를 생성하기에는 적합하지만 생각보다 객체 크기가 커서 (2KB 이상) 메모리가 부족한 시스템에서는 오히려 `minstd_rand` 가 적합할 수 있습니다.\sidenote{mt19937 를 생성한 이후에 난수를 생성하는 작업은 매우 빠릅니다.}

이처럼 난수 생성 엔진을 만들었지만 아직 바로 난수를 생성할 수 있는 것은 아닙니다. C++ 의 경우 어디에서 수들을 뽑아낼지 알려주는 **분포(distribution)** 을 정의해야 합니다.

앞서 우리의 경우 0 부터 99 까지 균등한 확률로 정수를 뽑아내고 싶다고 하였습니다. 따라서 이를 위해선 아래와 같이 균등 분포 (Uniform distribution) 객체를 정의해야 합니다.

```cpp-formatted
// 0 부터 99 까지 균등하게 나타나는 난수열을 생성하기 위해 균등 분포 정의.
std::uniform_int_distribution<int> dis(0, 99);
```

위와 같이 `uniform_int_distribution<int>` 의 생성자에 원하는 범위를 써넣으면 됩니다.

```cpp-formatted
for (int i = 0; i < 5; i++) {
  std::cout << "난수 : " << dis(gen) << std::endl;
}
```

그리고 마지막으로 균등 분포에 사용할 난수 엔진을 전달함으로써 균등 분포에서 무작위로 샘플을 뽑아낼 수 있습니다.

`<random>` 라이브러리에서는 균등 분포 말고도 여러가지 분포들을 제공하고 있습니다. 여기서는 다 일일히 소개하기 어렵지만 그 중 가장 많이 쓰이는 정규 분포 (Normal distribution) 만 간단히 살펴보겠습니다. (전체 목록은 [여기](https://en.cppreference.com/w/cpp/numeric/random)서 보세요.)

```cpp-formatted
#include <iomanip>
#include <iostream>
#include <map>
#include <random>

int main() {
  std::random_device rd;
  std::mt19937 gen(rd());
  std::normal_distribution<double> dist(/* 평균 = */ 0, /* 표준 편차 = */ 1);

  std::map<int, int> hist{};
  for (int n = 0; n < 10000; ++n) {
    ++hist[std::round(dist(gen))];
  }
  for (auto p : hist) {
    std::cout << std::setw(2) << p.first << ' '
              << std::string(p.second / 100, '*') << " " << p.second << '\n';
  }
}
```

성공적으로 컴파일 하였다면

```exec
-4  1
-3  38
-2 ****** 638
-1 ************************ 2407
 0 ************************************** 3821
 1 ************************ 2429
 2 ***** 595
 3  70
 4  1
```

코드를 보면 간단합니다.

```cpp-formatted
std::normal_distribution<double> dist(/* 평균 = */ 0, /* 표준 편차 = */ 1);
```

이번에는 평균 0 이고 표준 편차가 1 인 정규 분포를 정의하였고,

```cpp-formatted
for (int n = 0; n < 10000; ++n) {
  ++hist[std::round(dist(gen))];
}
```

이를 바탕으로 위와 같이 정규 분포에서 10000 개의 샘플을 무작위로 뽑아내게 됩니다. 실제로 위 그림 처럼 아름다은 정규 분포 곡선이 나옴을 확인할 수 있습니다.

자 그럼 이것으로 `random` 라이브러리 사용법을 간단히 알아보았습니다.

이번에는 C++ 11 에 같이 추가된 시간 관련 데이터를 쉽게 계산할 수 있도록 도와주는 `chrono` 라이브러리를 살펴보도록 하겠습니다.

### chrono 소개

`<chrono>` 는 크게 아래와 같이 3 가지 요소들로 구성되어 있습니다.

* 현재 시간을 알려주는 시계 - 예를 들어서 `system_clock`
* 특정 시간을 나타내는 `time_stamp`
* 시간의 간격을 나타내는 `duration`

로 말이지요.

### chrono 에서 지원하는 clock 들

`<chrono>` 에서는 여러가지 종류의 시계들을 지원하고 있습니다. 예를 들어서 일반적 상황에서 현재 컴퓨터 상 시간을 얻어 오기 위해서는 `std::system_clock` 을 사용하면 되고, 좀더 정밀한 시간 계산이 필요한 경우 (예를 들어 프로그램 성능을 측정하고 싶을 때) `std::high_resolution_clock` 을 사용하시면 됩니다.

이들 객체의 이름이 시계이기는 하지만 실제 시계 처럼 *지금 딱 몇 시* 이렇게 이야기 해주는 것이 아닙니다. 그 대신에, 지정된 시점으로 부터 **몇 번의 틱(tick)이 발생 하였는지 알려주는 `time_stamp` 객체** 를 리턴합니다. 예를 들어서 `std::system_clock` 의 경우 1970 년 1월 1일 부터 현재 까지 발생한 틱의 횟수를 리턴한다고 보시면 됩니다.\sidenote{이를 흔히 유닉스 타임(Unix time) 이라 부릅니다.} 쉽게 말해 `time_stamp` 객체는 `clock` 의 시작점과 현재 시간의 `duration` 을 보관하는 객체 입니다.

여기서 틱이라고 하면 시계의 초침이 한 번 똑딱 거리는 것이라 생각하면 됩니다. 컴퓨터의 경우도 내부에 시계가 있어서 특정 진동수로 똑딱 거리게 됩니다.\sidenote{우리나라는 시계가 똑딱 거린다고 하지만 미국에서는 tick-tock 이라 하죠}

각 시계 마다 정밀도가 다르기 때문에 각 `clock` 에서 얻어지는 `tick` 값 자체는 조금씩 다릅니다. 예를 들어서 `system_clock` 이 1 초에 1 tick 이라면, `high_resolution_clock` 의 경우 `0.00000001` 초 마다 1 tick 움직일 수 있습니다.

자 그렇다면 실제로 `chrono` 라이브러리를 사용하는 코드를 살펴보도록 하겠습니다. 아래 코드는 난수를 생성 속도를 측정합니다.

```cpp-formatted
#include <chrono>
#include <iomanip>
#include <iostream>
#include <random>
#include <vector>

int main() {
  std::random_device rd;
  std::mt19937 gen(rd());

  std::uniform_int_distribution<> dist(0, 1000);

  for (int total = 1; total <= 1000000; total *= 10) {
    std::vector<int> random_numbers;
    random_numbers.reserve(total);

    std::chrono::time_point<std::chrono::high_resolution_clock> start =
        std::chrono::high_resolution_clock::now();

    for (int i = 0; i < total; i++) {
      random_numbers.push_back(dist(gen));
    }

    std::chrono::time_point<std::chrono::high_resolution_clock> end =
        std::chrono::high_resolution_clock::now();

    // C++ 17 이전
    auto diff = end - start;

    // C++ 17 이후
    // std::chrono::duration diff = end - start; 

    std::cout << std::setw(7) << total
              << "개 난수 생성 시 틱 횟수 : " << diff.count() << std::endl;
  }
}
```

성공적으로 컴파일 하였다면

```exec
      1개 난수 생성 시 틱 횟수 : 535
     10개 난수 생성 시 틱 횟수 : 1370
    100개 난수 생성 시 틱 횟수 : 11354
   1000개 난수 생성 시 틱 횟수 : 110219
  10000개 난수 생성 시 틱 횟수 : 1145811
 100000개 난수 생성 시 틱 횟수 : 11040923
1000000개 난수 생성 시 틱 횟수 : 99170277
```

와 같이 나옵니다.

```cpp-formatted
std::chrono::time_point<std::chrono::high_resolution_clock> start =
  std::chrono::high_resolution_clock::now();
```

먼저 `high_resolution_clock` 으로 부터 현재의 `time_point` 를 얻어오는 코드부터 살펴봅시다. `chrono` 라이브러리의 경우 다른 표준 라이브러리와는 다르게 객체들이 `std::chrono` 이름 공간 안에 정의되어 있습니다. 따라서 `high_resolution_clock` 를 쓰기 위해서는 `std::high_resolution_clock` 가 아니라 `std::chrono::high_resolution_clock` 와 같이 적어야 합니다. 

만약에 매번 `std::chrono` 를 쓰기에 번거롭다면 그냥

```cpp
namespace ch = std::chrono;
```

와 같이 `ch` 라는 별명을 지어주고 ch 로 대체하면 됩니다.

이들 `clock` 에는 현재의 `time_point` 를 리턴하는 `static` 함수인 `now` 가 정의되어 있습니다. 이 `now()` 를 호출하면 위와 같이 해당 `clock` 에 맞는 `time_point` 객체를 리턴합니다. 우리의 경우 `high_resolution_clock::now()` 를 호출하였으므로, `std::chrono::time_point<ch::high_resolution_clock>` 를 리턴합니다.

`time_point` 가 `clock` 을 왜 템플릿 인자로 가지는지는 앞서도 설명하였듯이 `clock` 마다 1 초에 발생하는 틱 횟수가 모두 다르기 때문에 나중에 실제 시간으로 변환 시에 어떤 `clock` 을 사용했는지에 대한 정보가 필요하기 때문입니다.

```cpp-formatted
auto diff = end - start;
```

자 이제 난수 생성이 끝나면 `end` 에 끝나는 시간을 또 받아서 그 차이를 계산해야 합니다. 위와 같이 두 `time_stamp` 를 빼게 된다면 `duration` 객체를 리턴합니다.

```lec-warning
참고로 C++ 17 이전에서는 `end - start` 가 리턴하는 `duration` 객체의 템플릿 인자를 전달해야 합니다. 따라서 굳이 `duration` 의 템플릿 인자들을 지정하기 보다는 속시원하게 그냥 `auto diff = end - start` 로 하는게 낫습니다.
```

```cpp-formatted
std::cout << std::setw(7) << total
          << "개 난수 생성 시 틱 횟수 : " << diff.count() << std::endl;
```

`duration` 에는 `count` 라는 멤버 함수가 정의되어 있는데 이는 해당 시간 차이 동안 몇 번의 틱이 발생하였는지를 알려줍니다. 하지만 우리에게 좀 더 의미 있는 정보는 틱이 아니라 실제 시간으로 얼마나 걸렸는지 알아내는 것이지요. 이를 위해선 `duration_cast` 를 사용해야 합니다.

```cpp-formatted
#include <chrono>
#include <iomanip>
#include <iostream>
#include <random>
#include <vector>

namespace ch = std::chrono;

int main() {
  std::random_device rd;
  std::mt19937 gen(rd());

  std::uniform_int_distribution<> dist(0, 1000);

  for (int total = 1; total <= 1000000; total *= 10) {
    std::vector<int> random_numbers;
    random_numbers.reserve(total);

    ch::time_point<ch::high_resolution_clock> start =
        ch::high_resolution_clock::now();

    for (int i = 0; i < total; i++) {
      random_numbers.push_back(dist(gen));
    }

    ch::time_point<ch::high_resolution_clock> end =
        ch::high_resolution_clock::now();

    auto diff = end - start;
    std::cout << std::setw(7) << total << "개 난수 생성 시 걸리는 시간: "
              << ch::duration_cast<ch::microseconds>(diff).count() << "us"
              << std::endl;
  }
}
```

성공적으로 컴파일 하였다면

```exec
      1개 난수 생성 시 걸리는 시간: 0us
     10개 난수 생성 시 걸리는 시간: 1us
    100개 난수 생성 시 걸리는 시간: 10us
   1000개 난수 생성 시 걸리는 시간: 101us
  10000개 난수 생성 시 걸리는 시간: 1033us
 100000개 난수 생성 시 걸리는 시간: 10702us
1000000개 난수 생성 시 걸리는 시간: 98950us
```

와 같이 나옵니다.


```cpp-formatted
ch::duration_cast<ch::microseconds>(diff).count()
```

`duration_cast` 는 임의의 `duration` 객체를 받아서 우리가 원하는 `duration` 으로 캐스팅 할 수 있습니다. `std::chrono::microseconds` 는 `<chrono>` 에 미리 정의되어 있는 `duration` 객체 중 하나로, 1 초에 $$10^6$$ 번 틱을 하게 됩니다. 따라서 `microseconds` 로 캐스팅 한뒤에 리턴하는 `count` 값은 해당 `duration` 이 몇 마이크로초 인지를 나타내는 것이지요.

우리의 경우 1000000 개의 난수를 생성하는데 불과 98950 마이크로초, 대량 98 밀리초 정도 걸린다고 나왔습니다. `<chrono>` 에는 `std::chrono::microseconds` 외에도 `nanoseconds, milliseconds, seconds, minutes, hours` 가 정의되어 있기 때문에 상황에 맞게 사용하시면 됩니다.

### 현재 시간을 날짜로

안타깝게도 C++ 17 까지에서는 `chrono` 라이브러리 상에서 날짜를 간단하게 다룰 수 있도록 도와주는 클래스가 없습니다. 예를 들어서 현재 시간을 출력하고 싶다면 C 의 함수들에 의존해야 합니다.

예를 들어서 현재 시간을 출력하는 코드를 살펴봅시다.

```cpp-formatted
#include <chrono>
#include <ctime>
#include <iomanip>
#include <iostream>

int main() {
  auto now = std::chrono::system_clock::now();
  std::time_t t = std::chrono::system_clock::to_time_t(now);
  std::cout << "현재 시간은 : " << std::put_time(std::localtime(&t), "%F %T %z")
            << '\n';
}
```

성공적으로 컴파일 하였다면

```exec
현재 시간은 : 2020-01-06 00:28:08 +0900
```

먼저 `system_clock` 에서 현재의 `time_point` 를 얻어온 후에, 날짜를 출력하기 위해서 `time_t` 객체로 변환해야 합니다.

```cpp-formatted
std::time_t t = std::chrono::system_clock::to_time_t(now);
```

이를 위해 위와 같이 각 `clock` 이 제공하는 `static` 함수인 `to_time_t` 를 사용하면 됩니다.

```cpp-formatted
std::cout << "현재 시간은 : " << std::put_time(std::localtime(&t), "%F %T %z")
          << '\n';
```

그 다음에 현재 시간을 `std::tm` 객체로 변환하기 위해서 `std::localtime` 에 `t` 를 전달하였고, 마지막으로 `std::put_time` 을 사용해서 우리가 원하는 형태의 문자열로 구성할 수 있게 됩니다. 참고로 `put_time` 에 전달된 인자인 `"%F %T %z"` 는 `strftime` 에서 사용되는 인자와 동일합니다. 따라서 `%F` 와 같은 것들이 무엇을 수행하는지 알고 싶다면 해당 함수 레퍼런스를 참고하시기 바랍니다.

안타깝게도 C++ 17 현재 C 의 함수들을 이용하지 않고서 날짜를 다룰 수 있는 방법은 없습니다. 하지만 `C++ 20` 부터 `<chrono>` 에 C 라이브러리 필요 없이 날짜를 다룰 수 있는 클래스와 함수들이 추가된다고 하니 조금만 기다려주시기 바랍니다!

그렇다면 이번 강좌는 여기에서 마치도록 하겠습니다. 다음 강좌에서는 C++ 17 에서 등장한 `<filesystem>` 라이브러리를 살펴볼 것입니다.

```lec-summary
난수를 생성하기 위해서 C 의 `srand` 와 `rand` 를 사용하지 말자.
대부분의 상황에서는 `std::mt19937` 로 충분히 양질의 난수를 뽑아낼 수 있다. 특히 `<random>` 라이브러리를 사용할 경우 원하는 확률 분포에서 샘플링할 수 있다.
현재 시간을 알기 위해서는 `system_clock` 을 사용하면 되고, 좀더 정밀한 측정이 필요할 경우 `high_resolution_clock` 을 사용하면 된다.
`duration_cast` 를 이용해서 원하는 시간 스케일로 변환할 수 있다.
```

##@ chewing-cpp-end