그래서 저는 rand ()가 해로운 것으로 간주 되는 강연을 보았습니다. 그리고 그것은 단순 std::rand()
플러스 모듈러스 패러다임 보다 난수 생성의 엔진 배포 패러다임을 사용하도록 옹호했습니다 .
그러나 나는 std::rand()
직접 실패를보고 싶었 기 때문에 빠른 실험을했다.
- 기본적으로 2 개의 함수를 작성
getRandNum_Old()
했고getRandNum_New()
0에서 5 사이의 난수를 생성했습니다.std::rand()
하고std::mt19937
+std::uniform_int_distribution
각각. - 그런 다음 “이전”방식을 사용하여 960,000 (6으로 나눌 수있는) 난수를 생성하고 숫자 0-5의 빈도를 기록했습니다. 그런 다음 이러한 주파수의 표준 편차를 계산했습니다. 내가 찾고있는 것은 분포가 진정으로 균일 할 경우 일어날 일이기 때문에 가능한 한 낮은 표준 편차입니다.
- 이 시뮬레이션을 1000 번 실행하고 각 시뮬레이션에 대한 표준 편차를 기록했습니다. 또한 소요 시간을 밀리 초 단위로 기록했습니다.
- 그 후 다시 똑같이했지만 이번에는 “새로운”방식으로 난수를 생성했습니다.
- 마지막으로 구식과 신법 모두에 대한 표준 편차 목록의 평균과 표준 편차를 계산했고 구식과 신법 모두에 대해 취한 시간 목록의 평균과 표준 편차를 계산했습니다.
결과는 다음과 같습니다.
[OLD WAY]
Spread
mean: 346.554406
std dev: 110.318361
Time Taken (ms)
mean: 6.662910
std dev: 0.366301
[NEW WAY]
Spread
mean: 350.346792
std dev: 110.449190
Time Taken (ms)
mean: 28.053907
std dev: 0.654964
놀랍게도 롤의 총 스프레드는 두 방법 모두 동일했습니다. 즉, std::mt19937
+ std::uniform_int_distribution
는 단순한 것보다 “더 균일”하지 않았습니다.std::rand()
+%
. 내가 만든 또 다른 관찰은 새로운 것이 이전 방식보다 약 4 배 느리다는 것입니다. 전반적으로 품질 향상은 거의없이 속도면에서 엄청난 비용을 지불하는 것 같았습니다.
내 실험에 어떤 식 으로든 결함이 있습니까? 아니면std::rand()
정말 그렇게 나쁘지 않고 더 좋을까요?
참고로 다음은 전체적으로 사용한 코드입니다.
#include <cstdio>
#include <random>
#include <algorithm>
#include <chrono>
int getRandNum_Old() {
static bool init = false;
if (!init) {
std::srand(time(nullptr)); // Seed std::rand
init = true;
}
return std::rand() % 6;
}
int getRandNum_New() {
static bool init = false;
static std::random_device rd;
static std::mt19937 eng;
static std::uniform_int_distribution<int> dist(0,5);
if (!init) {
eng.seed(rd()); // Seed random engine
init = true;
}
return dist(eng);
}
template <typename T>
double mean(T* data, int n) {
double m = 0;
std::for_each(data, data+n, [&](T x){ m += x; });
m /= n;
return m;
}
template <typename T>
double stdDev(T* data, int n) {
double m = mean(data, n);
double sd = 0.0;
std::for_each(data, data+n, [&](T x){ sd += ((x-m) * (x-m)); });
sd /= n;
sd = sqrt(sd);
return sd;
}
int main() {
const int N = 960000; // Number of trials
const int M = 1000; // Number of simulations
const int D = 6; // Num sides on die
/* Do the things the "old" way (blech) */
int freqList_Old[D];
double stdDevList_Old[M];
double timeTakenList_Old[M];
for (int j = 0; j < M; j++) {
auto start = std::chrono::high_resolution_clock::now();
std::fill_n(freqList_Old, D, 0);
for (int i = 0; i < N; i++) {
int roll = getRandNum_Old();
freqList_Old[roll] += 1;
}
stdDevList_Old[j] = stdDev(freqList_Old, D);
auto end = std::chrono::high_resolution_clock::now();
auto dur = std::chrono::duration_cast<std::chrono::microseconds>(end-start);
double timeTaken = dur.count() / 1000.0;
timeTakenList_Old[j] = timeTaken;
}
/* Do the things the cool new way! */
int freqList_New[D];
double stdDevList_New[M];
double timeTakenList_New[M];
for (int j = 0; j < M; j++) {
auto start = std::chrono::high_resolution_clock::now();
std::fill_n(freqList_New, D, 0);
for (int i = 0; i < N; i++) {
int roll = getRandNum_New();
freqList_New[roll] += 1;
}
stdDevList_New[j] = stdDev(freqList_New, D);
auto end = std::chrono::high_resolution_clock::now();
auto dur = std::chrono::duration_cast<std::chrono::microseconds>(end-start);
double timeTaken = dur.count() / 1000.0;
timeTakenList_New[j] = timeTaken;
}
/* Display Results */
printf("[OLD WAY]\n");
printf("Spread\n");
printf(" mean: %.6f\n", mean(stdDevList_Old, M));
printf(" std dev: %.6f\n", stdDev(stdDevList_Old, M));
printf("Time Taken (ms)\n");
printf(" mean: %.6f\n", mean(timeTakenList_Old, M));
printf(" std dev: %.6f\n", stdDev(timeTakenList_Old, M));
printf("\n");
printf("[NEW WAY]\n");
printf("Spread\n");
printf(" mean: %.6f\n", mean(stdDevList_New, M));
printf(" std dev: %.6f\n", stdDev(stdDevList_New, M));
printf("Time Taken (ms)\n");
printf(" mean: %.6f\n", mean(timeTakenList_New, M));
printf(" std dev: %.6f\n", stdDev(timeTakenList_New, M));
}
답변
“old”의 거의 모든 구현은 LCG를rand()
사용합니다 . 그들은 일반적으로 최고의 생성기는 아니지만 일반적으로 이러한 기본 테스트에서 실패하는 것을 보지 않을 것입니다. 평균 및 표준 편차는 일반적으로 최악의 PRNG에서도 올바르게 얻습니다.
“나쁜”의 일반적인 실패-그러나 충분히 일반적- rand()
구현은 다음과 같습니다.
- 하위 비트의 낮은 임의성;
- 짧은 기간;
- 낮음
RAND_MAX
; - 연속 추출 사이의 일부 상관 관계 (일반적으로 LCG는 제한된 수의 하이퍼 플레인에있는 숫자를 생성하지만 어떻게 든 완화 될 수 있음).
그러나 이들 중 어느 것도 rand()
. 특정 구현은 xorshift 제품군 생성기를 srand
/ 뒤에 배치 할 수 rand
있으며 알고리즘 적으로 말하면 인터페이스 변경없이 최첨단 PRNG를 얻을 수 있으므로 수행 한 것과 같은 테스트는 출력에 약점을 표시하지 않습니다.
편집 : @R. rand
/ srand
인터페이스가를 srand
취하는 사실에 의해 제한된다는 사실을 정확하게 지적 unsigned int
하므로 구현이 그 뒤에 배치 할 수있는 모든 생성기는 본질적으로 UINT_MAX
가능한 시작 시드 (따라서 생성 된 시퀀스)로 제한됩니다 . API가 사소하게 확장 할 수 있지만 이것은 실제로 사실이다 srand
을 unsigned long long
, 또는 별도의 추가 srand(unsigned char *, size_t)
오버로드를.
실제로 실제 문제 는 원칙적rand()
으로 구현 이 많지 않지만 다음과 같습니다.
- 이전 버전과의 호환성; 많은 현재 구현은 일반적으로 잘못 선택된 매개 변수와 함께 차선의 생성기를 사용합니다. 악명 높은 예로 Visual C ++는
RAND_MAX
32767에 불과합니다. 그러나 이는 과거와의 호환성을 깨뜨리기 때문에 쉽게 변경할 수 없습니다.srand
재현 가능한 시뮬레이션을 위해 고정 된 시드를 사용 하는 사람들 은 너무 행복하지 않을 것입니다 (실제로 IIRC 앞서 언급 한 구현은 80 년대 중반부터 Microsoft C 초기 버전 또는 Lattice C로 돌아갑니다. -
단순한 인터페이스;
rand()
전체 프로그램에 대한 전역 상태를 가진 단일 생성기를 제공합니다. 이것은 많은 간단한 사용 사례에 대해 완벽하고 (실제로는 매우 편리함) 문제가 있습니다.- 멀티 스레드 코드 : 그것을 해결하기 위해 당신도 글로벌 뮤텍스 필요 – 아무 이유없이 모든 것을 느려지 게 하고 호출의 순서가 자체 무작위수록, 반복의 기회를 죽일를 – 또는 스레드 로컬 상태; 이 마지막 것은 여러 구현 (특히 Visual C ++)에서 채택되었습니다.
- 전역 상태에 영향을주지 않는 프로그램의 특정 모듈에 “비공개”, 재현 가능한 시퀀스를 원하는 경우.
마지막으로, rand
상황 :
- 실제 구현을 지정하지 않습니다 (C 표준은 샘플 구현 만 제공함). 따라서 다른 컴파일러에서 재현 가능한 출력을 생성하거나 알려진 품질의 PRNG를 기대하는 프로그램은 자체 생성기를 사용해야합니다.
- 괜찮은 시드를 얻을 수있는 크로스 플랫폼 방법을 제공하지 않습니다
time(NULL)
.
따라서 새로운 <random>
헤더는 다음과 같은 알고리즘을 제공하는이 혼란을 해결하려고합니다.
- 완전히 지정됨 (컴파일러 간 재현 가능한 출력 및 보장 된 특성 (예 : 생성기 범위)을 가질 수 있음)
- 일반적으로 최첨단 품질 ( 라이브러리가 설계되었을 때부터 ; 아래 참조);
- 클래스로 캡슐화 됨 (따라서 전역 상태가 강제 적용되지 않으므로 완전히 스레딩 및 비 지역성 문제를 피할 수 있음)
… 그리고 random_device
그들을 시드하기 위한 기본값 도 있습니다.
이제 “쉬운”, “수를 추측”하는 경우 (Python이 “복잡한”API를 제공하는 방법과 비슷하지만 사소한 & Co) 을 위해이 위에 구축 된 간단한 API 도 마음 에 들었을 것입니다. random.randint
. 우리가 빙고 카드의 숫자를 추출하고 싶을 때마다 임의의 장치 / 엔진 / 어댑터 / 무엇이든 빠져 나가고 싶지 않은 복잡하지 않은 사람들을 위해 글로벌 사전 시드 PRNG 사용),하지만 쉽게 할 수 있다는 것은 사실입니다. 현재 시설을 통해 직접 구축하십시오 (단순한 API를 통해 “전체”API를 구축하는 것은 불가능합니다).
마지막으로 성능 비교로 돌아가려면 : 다른 사람들이 지정한 것처럼 빠른 LCG를 느린 (그러나 일반적으로 더 나은 품질로 간주되는) Mersenne Twister와 비교합니다. LCG의 품질에 문제가 없으면 std::minstd_rand
대신 std::mt19937
.
실제로, std::minstd_rand
초기화에 쓸모없는 정적 변수 를 사용 하고 피 하도록 함수를 조정 한 후
int getRandNum_New() {
static std::minstd_rand eng{std::random_device{}()};
static std::uniform_int_distribution<int> dist{0, 5};
return dist(eng);
}
9ms (이전) 대 21ms (신규)를 얻습니다. 마지막으로, dist
(전통적인 모듈로 연산자에 비해 입력 범위의 배수가 아닌 출력 범위에 대한 분포 왜곡을 처리하는) 제거하고 수행중인 작업으로 돌아 가면getRandNum_Old()
int getRandNum_New() {
static std::minstd_rand eng{std::random_device{}()};
return eng() % 6;
}
나는 6 MS에 내려 (그래서, 30 % 빠른)에 대한 호출과는 달리 아마 때문에 rand()
, std::minstd_rand
인라인에 용이하다.
덧붙여서, 나는 손으로 말아서 (그러나 표준 라이브러리 인터페이스를 거의 준수하는) 동일한 테스트를했고 XorShift64*
, rand()
(3.68 ms vs 8.61 ms) 보다 2.3 배 빠릅니다 . 메르 센 트위스터와 다양한 제공 LCGs 달리이 주어진 비행 색상으로 현재의 임의성 테스트 스위트를 통과 하고 이 엄청나게 빨리, 그것은 아직 표준 라이브러리에 포함되지 않은 이유가 궁금합니다.
답변
5보다 큰 범위로 실험을 반복하면 아마도 다른 결과가 나타날 것입니다. 범위가 다음보다 훨씬 작을 때RAND_MAX
대부분의 응용 프로그램에 문제가없는 .
예를 들어 a RAND_MAX
가 25이면 rand() % 5
다음 빈도로 숫자를 생성합니다.
0: 6
1: 5
2: 5
3: 5
4: 5
마찬가지로 RAND_MAX
이상 32,767 보장하고, 가장 가능성이 가장 가능성 사이의 주파수의 차이입니다 분포가 대부분의 사용 사례에 대한 임의 충분히 가까이 작은 숫자 만 1입니다.
답변
첫째, 놀랍게도 난수를 사용하는 용도에 따라 답이 달라집니다. 예를 들어 임의의 배경색 변경자를 구동하려면 rand ()를 사용하는 것이 좋습니다. 임의의 숫자를 사용하여 임의의 포커 핸드 또는 암호화 보안 키를 만드는 경우에는 문제가 없습니다.
예측 가능성 : 시퀀스 012345012345012345012345 …는 표본의 각 숫자에 대한 균등 한 분포를 제공하지만 분명히 무작위가 아닙니다. 시퀀스가 임의적이면 n + 1의 값은 n의 값 (또는 n, n-1, n-2, n-3 등의 값으로도)으로 쉽게 예측할 수 없습니다. 분명히 반복되는 시퀀스 동일한 자릿수의 경우 퇴화되는 경우이지만 선형 합동 생성기로 생성 된 시퀀스는 분석 대상이 될 수 있습니다. 공용 라이브러리에서 공용 LCG의 기본 설정을 사용하면 악의적 인 사람이 별다른 노력없이 “시퀀스를 깨뜨릴”수 있습니다. 과거에는 여러 온라인 카지노 (및 일부 오프라인 카지노)가 열악한 난수 생성기를 사용하는 기계에 의해 손실을 입었습니다. 더 잘 알아야 할 사람들조차 따라 잡았습니다.
분포 : 비디오에서 언급했듯이 모듈로 100 (또는 시퀀스 길이로 균등하게 나눌 수없는 값)을 사용하면 일부 결과가 다른 결과보다 약간 더 높은 가능성을 보장 할 수 있습니다. 가능한 시작 값이 100 모듈로 32767 인 우주에서 0에서 66까지의 숫자는 67에서 99까지의 값보다 328/327 (0.3 %) 더 자주 나타납니다. 공격자에게 이점을 제공 할 수있는 요소입니다.
답변
정답은 “더 좋음”이 의미하는 바에 따라 다릅니다.
“새로운” <random>
엔진은 13 년 전에 C ++에 도입되었으므로 실제로는 새로운 엔진이 아닙니다. C 라이브러리rand()
는 수십 년 전에 소개되었으며 그 당시에는 여러 가지에 매우 유용했습니다.
C ++ 표준 라이브러리는 세 가지 클래스의 난수 생성기 엔진을 제공합니다. rand()
예제), Lagged Fibonacci 및 Mersenne Twister의 . 각 클래스에는 장단점이 있으며 각 클래스는 특정 방식에서 “최고”입니다. 예를 들어, LCG는 상태가 매우 작으며 올바른 매개 변수를 선택하면 최신 데스크탑 프로세서에서 상당히 빠릅니다. LFG는 더 큰 상태를 가지며 메모리 가져 오기 및 추가 작업 만 사용하므로 특수 수학 하드웨어가없는 임베디드 시스템 및 마이크로 컨트롤러에서 매우 빠릅니다. MTG는 상태가 크고 느리지 만 스펙트럼 특성이 뛰어난 매우 큰 비 반복 시퀀스를 가질 수 있습니다.
제공된 생성기가 특정 용도에 적합하지 않은 경우 C ++ 표준 라이브러리는 하드웨어 생성기 또는 자체 사용자 정의 엔진에 대한 인터페이스도 제공합니다. 생성기 중 어느 것도 독립형으로 사용할 수 없습니다. 특정 확률 분포 함수와 함께 무작위 시퀀스를 제공하는 분포 객체를 통해 사용됩니다.
<random>
over의 또 다른 장점 은 전역 상태 rand()
를 rand()
사용하고 재진입 또는 스레드 안전이 아니며 프로세스 당 단일 인스턴스를 허용한다는 것입니다. 세분화 된 제어 또는 예측 가능성 (즉, RNG 시드 상태에서 버그를 재현 할 수 있음)이 필요한 경우 rand()
에는 쓸모가 없습니다. <random>
발전기 로컬 인스턴스화 및 직렬화 (및 복원 가능한) 상태가된다.