โค้ดตัวอย่างนี้แสดงให้เห็นว่าstd::rand
เป็นกรณีของ Balderdash ลัทธิการขนส่งสินค้าแบบดั้งเดิมที่ควรทำให้คิ้วของคุณยกขึ้นทุกครั้งที่คุณเห็น
มีปัญหาหลายประการที่นี่:
คนทำสัญญามักจะถือว่า - แม้แต่วิญญาณผู้เคราะห์ร้ายที่ไม่รู้จักอะไรดีกว่าและจะไม่คิดในแง่เหล่านี้อย่างชัดเจนนั่นคือrand
ตัวอย่างจากการแจกแจงสม่ำเสมอบนจำนวนเต็มใน 0, 1, 2, ... RAND_MAX
, และการโทรแต่ละครั้งจะให้ตัวอย่างอิสระ
ปัญหาแรกคือสัญญาที่สมมติขึ้นซึ่งเป็นตัวอย่างแบบสุ่มที่เป็นอิสระในการโทรแต่ละครั้งไม่ใช่สิ่งที่เอกสารระบุไว้จริง ๆ และในทางปฏิบัติการนำไปใช้งานในอดีตล้มเหลวในการจัดหาแม้แต่สถานการณ์จำลองของความเป็นอิสระที่ชัดเจนที่สุด ตัวอย่างเช่น C99 §7.20.2.1 'The rand
function' กล่าวโดยไม่ต้องอธิบายรายละเอียด:
rand
ฟังก์ชั่นคำนวณลำดับของจำนวนเต็มสุ่มหลอกในช่วง 0 RAND_MAX
ถึง
นี่เป็นประโยคที่ไม่มีความหมายเนื่องจาก pseudorandomness เป็นคุณสมบัติของฟังก์ชัน (หรือตระกูลของฟังก์ชัน ) ไม่ใช่จำนวนเต็ม แต่ก็ไม่ได้หยุดแม้แต่เจ้าหน้าที่ ISO จากการใช้ภาษาในทางที่ผิด ท้ายที่สุดผู้อ่านเพียงคนเดียวที่จะไม่พอใจมันรู้ดีกว่าอ่านเอกสารrand
เพราะกลัวว่าเซลล์สมองจะสลายตัว
การใช้งานทางประวัติศาสตร์โดยทั่วไปใน C จะทำงานดังนี้:
static unsigned int seed = 1;
static void
srand(unsigned int s)
{
seed = s;
}
static unsigned int
rand(void)
{
seed = (seed*1103515245 + 12345) % ((unsigned long)RAND_MAX + 1);
return (int)seed;
}
สิ่งนี้มีคุณสมบัติที่น่าเสียดายที่แม้ว่าตัวอย่างเดียวอาจมีการกระจายอย่างสม่ำเสมอภายใต้เมล็ดพันธุ์แบบสุ่มที่เหมือนกัน (ซึ่งขึ้นอยู่กับค่าเฉพาะของRAND_MAX
) มันสลับระหว่างจำนวนเต็มคู่และจำนวนคี่ในการโทรติดต่อกัน - หลัง
int a = rand();
int b = rand();
นิพจน์(a & 1) ^ (b & 1)
จะให้ 1 พร้อมความน่าจะเป็น 100% ซึ่งไม่ใช่กรณีสำหรับตัวอย่างสุ่มอิสระในการแจกแจงใด ๆ ที่รองรับบนจำนวนเต็มคู่และจำนวนคี่ ดังนั้นลัทธิการขนส่งสินค้าจึงเกิดขึ้นว่าเราควรทิ้งบิตลำดับต่ำเพื่อไล่ล่าสัตว์ร้ายที่เข้าใจยากของ 'การสุ่มที่ดีกว่า' (คำเตือนเกี่ยวกับสปอยเลอร์: นี่ไม่ใช่ศัพท์ทางเทคนิคนี่เป็นสัญญาณว่าใครก็ตามที่คุณอ่านร้อยแก้วไม่ทราบว่าพวกเขากำลังพูดถึงอะไรหรือคิดว่าคุณเป็นคนไร้เดียงสาและต้องถูกมองข้าม)
ปัญหาที่สองคือแม้ว่าการเรียกแต่ละครั้งจะทำตัวอย่างเป็นอิสระจากการแจกแจงแบบสุ่มที่สม่ำเสมอบน 0, 1, 2, …, RAND_MAX
ผลลัพธ์ของrand() % 6
การกระจายจะไม่สม่ำเสมอใน 0, 1, 2, 3, 4, 5 เหมือนการตาย ม้วนเว้นแต่RAND_MAX
จะสอดคล้องกับ -1 โมดูโล 6 ตัวอย่าง ง่ายๆตัวอย่าง: ถ้าRAND_MAX
= 6 จากนั้นrand()
ผลลัพธ์ทั้งหมดมีความน่าจะเป็นเท่ากับ 1/7 แต่จากrand() % 6
ผลลัพธ์ 0 มีความน่าจะเป็น 2/7 ในขณะที่ผลลัพธ์อื่น ๆ ทั้งหมดมีความน่าจะเป็น 1/7 .
วิธีที่จะทำเช่นนี้คือมีการสุ่มตัวอย่างปฏิเสธ: ซ้ำ ๆวาดอิสระตัวอย่างสุ่มเครื่องแบบs
จาก 0, 1, 2, ... , RAND_MAX
และปฏิเสธ (ตัวอย่าง) ผลลัพธ์ที่ 0, 1, 2, ... , ((RAND_MAX + 1) % 6) - 1
ถ้าคุณได้รับหนึ่ง เริ่มต้นใหม่ s % 6
มิฉะนั้นผลผลิต
unsigned int s;
while ((s = rand()) < ((unsigned long)RAND_MAX + 1) % 6)
continue;
return s % 6;
วิธีนี้ชุดของผลลัพธ์rand()
ที่เรายอมรับจะหารด้วย 6 เท่า ๆ กันและผลลัพธ์ที่เป็นไปได้แต่ละรายการจะได้มาจากs % 6
จำนวนผลลัพธ์ที่ยอมรับจากจำนวนเท่ากันrand()
ดังนั้นหากrand()
มีการกระจายอย่างสม่ำเสมอก็จะเป็นs
เช่นนั้น ไม่มีข้อผูกมัดกับจำนวนการทดลอง แต่จำนวนที่คาดหวังน้อยกว่า 2 และความน่าจะเป็นที่จะประสบความสำเร็จเพิ่มขึ้นแบบทวีคูณตามจำนวนการทดลอง
ทางเลือกของซึ่งผลลัพธ์ของrand()
คุณปฏิเสธไม่เป็นสาระสำคัญโดยมีเงื่อนไขว่าคุณแมจำนวนที่เท่ากันของพวกเขาไปยังแต่ละจำนวนเต็มด้านล่าง 6. รหัสที่ cppreference.com ที่ทำให้แตกต่างกันทางเลือกเพราะปัญหาแรกข้างต้นว่าไม่มีอะไรที่จะรับประกันเกี่ยวกับ การกระจายหรือความเป็นอิสระของเอาต์พุตของrand()
และในทางปฏิบัติบิตลำดับต่ำจะแสดงรูปแบบที่ไม่ 'ดูสุ่มเพียงพอ' (ไม่ต้องสนใจว่าเอาต์พุตถัดไปเป็นฟังก์ชันที่กำหนดไว้ก่อนหน้านี้)
การออกกำลังกายสำหรับผู้อ่าน: พิสูจน์ว่ารหัสที่ cppreference.com ผลผลิตเครื่องแบบกระจายม้วนตายถ้าrand()
อัตราผลตอบแทนเครื่องแบบกระจายกับ 0, 1, 2, ... RAND_MAX
,
แบบฝึกหัดสำหรับผู้อ่าน: เหตุใดคุณจึงชอบที่จะปฏิเสธหนึ่งชุดหรือชุดย่อยอื่น ๆ การคำนวณแบบใดที่จำเป็นสำหรับการพิจารณาคดีแต่ละครั้งในสองกรณีนี้
ปัญหาที่สามคือพื้นที่เมล็ดมีขนาดเล็กมากแม้ว่าเมล็ดพันธุ์จะกระจายอย่างสม่ำเสมอ แต่ฝ่ายตรงข้ามที่มีความรู้เกี่ยวกับโปรแกรมของคุณและผลลัพธ์เพียงอย่างเดียว แต่ไม่ใช่เมล็ดพันธุ์สามารถทำนายเมล็ดพันธุ์และผลลัพธ์ที่ตามมาได้ทันทีซึ่งทำให้ดูเหมือนว่าไม่เป็นเช่นนั้น สุ่มหลังจากทั้งหมด ดังนั้นอย่าคิดที่จะใช้สิ่งนี้สำหรับการเข้ารหัส
คุณสามารถไปตามเส้นทางที่ใช้งานง่ายและคลาส C ++ 11 std::uniform_int_distribution
ด้วยอุปกรณ์สุ่มที่เหมาะสมและเอนจินสุ่มที่คุณชื่นชอบเช่น Mersenne twister ยอดนิยมstd::mt19937
เพื่อเล่นลูกเต๋ากับลูกพี่ลูกน้องวัยสี่ขวบของคุณ แต่ถึงอย่างนั้นก็จะไม่ไป เหมาะสำหรับการสร้างวัสดุคีย์การเข้ารหัส - และ Mersenne twister ก็เป็นหมูอวกาศที่น่ากลัวเช่นกันด้วยสถานะหลายกิโลไบต์ที่สร้างความหายนะให้กับแคชของ CPU ของคุณด้วยเวลาตั้งค่าที่หยาบคายดังนั้นจึงไม่ดีแม้กระทั่งสำหรับเช่นการจำลองมอนติคาร์โลแบบขนานกับ ต้นไม้ที่ทำซ้ำได้ของการคำนวณย่อย ความนิยมน่าจะเกิดจากชื่อที่ติดปากเป็นหลัก แต่คุณสามารถใช้สำหรับทอยลูกเต๋ากลิ้งได้ดังตัวอย่างนี้!
อีกวิธีหนึ่งคือการใช้ตัวสร้างตัวเลขหลอกรหัสลับแบบธรรมดาที่มีสถานะขนาดเล็กเช่นPRNG การลบคีย์อย่างรวดเร็วอย่างง่ายหรือเพียงแค่การเข้ารหัสแบบสตรีมเช่น AES-CTR หรือ ChaCha20 หากคุณมั่นใจ ( เช่นในการจำลองมอนติคาร์โลสำหรับ การวิจัยทางวิทยาศาสตร์ธรรมชาติ) ว่าไม่มีผลเสียใด ๆ ในการทำนายผลลัพธ์ที่ผ่านมาหากรัฐถูกบุกรุก
std::uniform_int_distribution