Language/C++

[C++] 난수 생성

nowkoes 2023. 9. 4. 10:04

랜덤 함수

개요

 컴퓨터는 명령어와 알고리즘을 기반으로 동작하는 결정론적인 기계다. 이런 특성 때문에, 컴퓨터는 완전히 무작위적인 값을 자체적으로 생성하는 것이 기본적으로 어렵다. 그렇지만, 멀티코어 컴퓨터의 경우 각 코어 간의 타이밍 차이나 상호 작용으로 인한 비결정성을 활용하여 어느 정도의 무작위성을 가진 값을 생성할 수 있다. 그렇지만 이렇게 생성된 값은 완전한 엔트로피를 기반으로 하는 '진정한' 난수에 비해 일정한 제한이 있을 수 있다.

 C++에서는 여러 방법으로 이러한 무작위 값을 생성할 수 있다. 대표적인 방법으로는 C-style의 rand() 함수, C++11 이후에 도입된 <random> 라이브러리, 그리고 하드웨어 기반의 난수 생성기를 사용하는 std::random_device 클래스 등이 있다. 이 글에서는 C++에서 난수를 생성하는 다양한 방법을 살펴보겠다.


본문

rand() 함수 활용

출처: cppreference

 

 rand() 함수는 0부터 RAND_MAX(0x7FFF = 32767)까지의 슈도-랜덤 정수를 반환하는 함수다. 이를 이용해 랜덤한 수를 10번 출력하는 코드를 만들면 다음과 같다.

 

#include <iostream>
#include <cstdlib>
#include <ctime>

int main()
{
	std::cout << "random number by using rand\n";
	for (int i = 0; i < 10; ++i)
		std::cout << rand() % 100 << "\n";

	return 0;
}

출력 결과

 

 하지만 이 함수에는 치명적인 문제가 있다. 만약 첫 출력에서 위의 <출력 결과>와 같이 나왔다면, 프로그램을 여러 번 실행하더라도 생성된 난수의 시퀀스가 항상 동일하게 시작된다. 이는 rand() 함수가 의사난수 생성기의 일종으로서, 내부의 알고리즘과 초기값을 사용하여 난수처럼 보이는 숫자 시퀀스를 생성하기 때문이다.

 

 즉, rand() 함수는 프로그램이 시작될 때 자동으로 초기화되는데, 이 초기화 값(시드(seed): 난수 생성기의 초기 상태를 결정하는 값)은 항상 동일하다. 따라서 프로그램을 여러 번 실행하더라도 결과가 똑같이 출력되는 것이다. 이를 해결하기 위해 시드값을 변경하여 난수 생성기의 시작점을 다르게 설정하면 해결할 수 있다. 보통, 현재 시간을 시드값으로 초기화하는 방법을 자주 사용한다.

 

#include <iostream>
#include <cstdlib>
#include <ctime>

int main()
{
	srand(static_cast<unsigned int>(time(NULL))); 
    // 현재 시간 time(NULL)을 srand()에 전달하여 rand()의 시드값을 변경

	std::cout << "random number by using srand\n";
	for (int i = 0; i < 10; ++i)
		std::cout << rand() % 100 << "\n";

	return 0;
}

출력 결과

 

 이렇게 하면, 프로그램을 다시 시작할 때마다 시간이 바뀌므로 다른 난수 시퀀스를 얻게 되어 다른 결과를 얻을 수 있다.

 

 이렇게 단순히 시드값을 초기화해 주면 바로 활용 가능하여 간단하고 빠르다는 장점을 가진 rand() 함수 방법은, 난수의 품질이 좋지 않고 낮은 비트에서는 덜 무작위적일 수 있어 특정 프로그램에서 문제가 발생할 수 있다는 단점이 있다. 이는 심화적인 내용인데, 더 알고 싶으면 아래의 접은 글을 확인해 보자.

 

더보기

 난수의 품질이 좋지 않다는 의미는, 난수 생성기의 출력이 완벽하게 균일한 분포를 가지지 않을 때, 또는 일부 패턴이나 규칙성을 포함할 수 있다는 것을 의미한다. 예를 들어, 완벽한 균일 분포 난수 생성기는 0부터 9까지의 숫자를 균등하게 생성한다. 그러나 품질이 좋지 않은 난수 생성기는 어떤 숫자를 다른 숫자보다 자주 생성할 수 있다.

 

 낮은 비트에서는 덜 무작위적이다는 의미는, 난수 생성기가 생성하는 값은 보통 여러 비트로 구성된다. 여기서 낮은 비트란 이진수의 표현에서 가장 오른쪽에 위치한 비트를 의미한다. 예를 들어, 숫자 13의 이진 표현은 1101이므로, 여기서 낮은 비트는 1이다. 

 rand()와 같은 오래된 방식은, 이러한 낮은 비트들에서 규칙성이나 패턴을 보일 수가 있다. 이는 선형 합동 방법(Linear Congruential Generator, LCG) 같은 간단한 알고리즘에 기반하여 난수를 생성한다는 점에서 기인한다. 

 해당 알고리즘을 수식화하면 X_n+1 = (aX_n + c) mod m과 같이 되는데, 여기서 파라미터를 잘못 선택하면 주기적인 패턴을 보이게 되며, 알고리즘의 선형적인 특성 때문에 낮은 비트들이 규칙성을 보인다. 이러한 내용을 담은 논문(추후에 링크 추가할 예정)을 읽어 보면 도움이 될 것이다.


random 라이브러리 활용

출처: cppreference

 

 C++11 이후부터는 random 라이브러리를 제공한다. 해당 라이브러리는 다양한 난수 생성 알고리즘을 제공하는데, 여기서 가장 널리 사용되는 Mersenne Twister 알고리즘을 기준으로 설명하겠다. 해당 알고리즘은 앞서 설명한 random 함수와 다르게 품질이 높고 원하는 유형의 난수를 생성하고 있어, 가장 많이 사용된다.

 

#include <iostream>
#include <random>

int main() 
{
    std::default_random_engine generator;
    std::uniform_int_distribution<int> distribution(0, 100);

    for (int i = 0; i < 10; ++i)
        std::cout << distribution(generator) << "\n";

    return 0;
}

출력

 

 해당 코드는 1에서 100 사이의 정수 난수를 생성하는 예제다. std::default_random_engine은 C++11에서 제공하는 기본 난수 엔진 중 하나다. 이 엔진 객체 generator는 난수를 생성하기 위한 상태를 유지한다. 즉, 난수 생성기가 내부적으로 일련의 값을 생성하는 데 사용하는 변수나 데이터를 갖고 있다는 것을 의미한다. 이 상태는 난수 생성의 결과에 영향을 끼치며, 상태의 변화에 따라 다음에 생성될 난수가 결정된다.

 그리고 std::uniform_int_distribution은 균일 분포를 가진 정수 난수를 생성하는 템플릿 클래스다. 이 경우, int로 템플릿 인수가 주어져서 정수를 반환하는 분포 객체를 생성한다. 

 마지막으로 distribution 객체의 연산자 operator()는 인자로 주어진 난수 엔진 generator를 사용하여 난수를 생성한다. 


random_device

출처: cppreference

 

 std::random_device는 C++11 이후로 제공되는 클래스로, 하드웨어 기반의 엔트로피 소스를 활용할 수 있는 기능을 제공한다. 즉, 시스템의 물리적인 장치나 특성을 이용하여 난수를 생성하는데, 이를 통해 생성된 난수는 이전의 Mersenne Twister 알고리즘으로 생성된 것보다 더욱 높은 품질을 보장한다. 대신 하드웨어에 의존한다는 특성으로 인해 모든 플랫폼이나 시스템에서 동일한 방식으로 동작하지 않을 수 있고, 엔트로피 소스에 따라 난수 생성 속도가 느릴 수 있다는 단점이 존재한다.

 

#include <iostream>
#include <random>

int main() 
{
    std::random_device rd;
    std::uniform_int_distribution<int> distribution(0, 100);

    for (int i = 0; i < 10; ++i)
        std::cout << distribution(rd) << "\n";

    return 0;
}

출력


요약

난수
1. 정의: 무작위 값
2. 생성법
 a. rand(): srand() 함수와 time(NULL) 시드 값을 이용해 생성하는 방법
  - 장점: 예전 코드와 호환이 잘 되며, 간단하게 생성 가능
  - 단점: 선형적인 특성으로 인해, 결국 어떠한 규칙을 따라가게 되어 있어 품질이 낮음
 b. random library: Mersenne Twister 알고리즘을 주로 사용하며, generator와 distribution을 이용하여 난수 생성
  - 장점: rand() 함수보다 높은 품질을 보장하며, 다양한 유형의 난수를 생성할 수 있음
  - 단점: 사용법이 복잡할 수 있음
 c. random_device: 하드웨어적 특성에 의존하여 엔트로피 소스를 사용하는 난수 생성기
  - 장점: random library보다 더욱 높은 품질을 보장
  - 단점: 하드웨어에 의존하는 특성상, 모든 플랫폼에서 균일한 성능을 보장할 수 없음

 

반응형