Linuxでは、rand()が数値を繰り返す頻度がMacよりはるかに多いのはなぜですか?


87

私が取り組んでいるプロジェクトの一部としてCにハッシュマップを実装していて、ランダム挿入を使用してそれをテストしていたのはrand()、Linuxの方がMacよりはるかに頻繁に数値を繰り返すようであることに気付いたときです。RAND_MAX両方のプラットフォームで2147483647 / 0x7FFFFFFFです。私はそれを、バイト配列RAND_MAX+1-longを作成し、RAND_MAX乱数を生成し、それぞれが重複であるかどうかをメモし、見られるようにリストからそれをチェックするこのテストプログラムに削減しました。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

int main() {
    size_t size = ((size_t)RAND_MAX) + 1;
    char *randoms = calloc(size, sizeof(char));
    int dups = 0;
    srand(time(0));
    for (int i = 0; i < RAND_MAX; i++) {
        int r = rand();
        if (randoms[r]) {
            // printf("duplicate at %d\n", r);
            dups++;
        }
        randoms[r] = 1;
    }
    printf("duplicates: %d\n", dups);
}

Linuxは常に約7億9千万の複製を生成します。Macは常に1つしか生成ないため、ほとんど繰り返すことなく生成できるすべての乱数をループします。誰も私にこれがどのように機能するか説明してくれませんか?manページとの違いは何もわかりません。それぞれが使用しているRNGもわかりませんし、オンラインでも何も見つかりません。ありがとう!


4
rand()は0..RAND_MAXからの値を返すため、配列のサイズをRAND_MAX + 1
Blastfurnace

21
RAND_MAX / e〜= 7億9,000万であることに気付いたかもしれません。また、nが無限大に近づくときの(1-1 / n)^ nの制限は1 / eです。
デビッドシュワルツ

3
@DavidSchwartz私があなたを正しく理解していれば、Linuxの数値が一貫して約7億9000万である理由を説明しているかもしれません。私はその時の質問は、どうして/なぜMac 何度繰り返さないのでしょうか?
セロンS

26
ランタイムライブラリでは、PRNGの品質要件はありません。実際の要件は、同じシードでの再現性のみです。どうやら、LinuxのPRNGの品質はMacよりも優れています。
pmg

4
@chuxはい、ただし乗算に基づいているため、状態がゼロになることはなく、結果(次の状態)もゼロになる可能性があります。ソースコードに基づいて、ゼロでシードされている場合、特別なケースとしてゼロをチェックしますが、シーケンスの一部としてゼロを生成しません。
Arkku

回答:


119

最初はmacOS rand()が数字を繰り返さない方が何とか優れているように思えるかもしれませんが、この数の数字が生成されると、大量の重複が発生すると予想されることに注意してください(実際、約7億9000万、または(2 31 -1 )/ e)。同様に、番号を順番に反復しても重複は発生しませんが、非常にランダムであるとは見なされません。したがって、このテストでrand()はLinuxの実装は真のランダムソースと区別できませんが、macOS はそうではありません。rand()

一見すると驚くべきもう1つのことは、macOS rand()が重複をうまく回避する方法です。そのソースコードを見ると、実装は次のようになっています。

/*
 * Compute x = (7^5 * x) mod (2^31 - 1)
 * without overflowing 31 bits:
 *      (2^31 - 1) = 127773 * (7^5) + 2836
 * From "Random number generators: good ones are hard to find",
 * Park and Miller, Communications of the ACM, vol. 31, no. 10,
 * October 1988, p. 1195.
 */
    long hi, lo, x;

    /* Can't be initialized with 0, so use another value. */
    if (*ctx == 0)
        *ctx = 123459876;
    hi = *ctx / 127773;
    lo = *ctx % 127773;
    x = 16807 * lo - 2836 * hi;
    if (x < 0)
        x += 0x7fffffff;
    return ((*ctx = x) % ((unsigned long) RAND_MAX + 1));

これによりRAND_MAX、シーケンスが再び繰り返される前に、1からまでのすべての数値が1 回だけ含まれます。次の状態は乗算に基づいているため、状態がゼロになることはありません(または将来の状態もすべてゼロになります)。したがって、表示される繰り返し数は最初の数であり、ゼロは決して返されない数です。

Appleは、少なくともmacOS(またはOS X)が存在する限り、ドキュメントと例でより優れた乱数ジェネレーターの使用を促進しrand()てきました。利用可能な最も単純な疑似乱数ジェネレータ。(あなたが指摘したように、彼らrand()arc4random()代わりに使用するための推奨事項でさえコメントされています。)

関連するノートでは、このランダム性のテスト(および他の多くのテスト)で適切な結果を生成する、最も簡単な疑似乱数ジェネレータはxorshift *です。

uint64_t x = *ctx;
x ^= x >> 12;
x ^= x << 25;
x ^= x >> 27;
*ctx = x;
return (x * 0x2545F4914F6CDD1DUL) >> 33;

この実装により、テストではほぼ正確に7億9千万の重複が発生します。


5
雑誌記事 1980年代に発表されたが、「誕生日の問題」に基づいてのPRNGのための統計的検定を提案しました。
pjs

14
「アップルは、ドキュメントでより優れた乱数ジェネレータの使用を促進しています」->もちろん、アップルは、arc4random()背後にあるようなコードを採用rand()して、良いrand()結果を得ることができます。プログラマーに別の方法でコードを作成させるのではなく、より優れたライブラリー関数を作成するだけです。「スタックしたばかり」が彼らの選択です。
chux-モニカを

23
Macには定数オフセットrand()がないため、実際の使用には役立ちません。なぜrand()%7は常に0を返すのですか?Rand()%14は、値6または13のみを生成します
phuclv

4
@PeterCordes:にはrand、同じシードで再実行して同じシーケンスを生成するという要件があります。OpenBSD randは壊れており、この契約には従いません。
R .. GitHub ICE HELPING ICE

8
@ R..GitHubSTOPHELPINGICE rand()同じシードを使用して、異なるバージョンのライブラリ間で同じシーケンスを生成するというCの要件はありますか このような保証は、ライブラリバージョン間の回帰テストに役立つ可能性がありますが、Cの要件はありません。
chux-モニカを

34

MacOSは、ドキュメント化されていないrand()関数をstdlibに提供します。シードしないでおくと、出力される最初の値は16807、282475249、1622650073、984943658、1144108930になります。クイック検索では、このシーケンスが次の式を繰り返す非常に基本的なLCG乱数ジェネレータに対応していることがわかります。

x n +1 = 7 5x n(mod 2 31 − 1)

このRNGの状態は単一の32ビット整数の値によって完全に記述されるため、その周期はそれほど長くありません。正確には、全ての2繰り返さ31 1から2までのすべての値を出力し、2回の反復- 31 2 - 。

Linuxのすべてのバージョンにrand()の標準実装があるとは思いませんが、頻繁に使用されるglibc rand()関数があります。これは、単一の32ビット状態変数の代​​わりに、1000ビットを超えるプールを使用します。これは、すべての意図と目的に対して、完全に繰り返されるシーケンスを生成することは決してありません。この場合も、最初にシードせずにこのRNGの最初のいくつかの出力を印刷することで、現在のバージョンを確認できます。(glibc rand()関数は、数値1804289383、846930886、1681692777、1714636915および1957747793を生成します。)

したがって、Linux(およびMacOSではほとんど衝突しない)で衝突が増えるのは、Linuxバージョンのrand()が基本的にランダムであるためです。


5
rand()srand(1);
シードされ

5
rand()macOS ののソースコードが利用可能です:opensource.apple.com/source/Libc/Libc-1353.11.2/stdlib/FreeBSD/…FWIW、ソースからコンパイルしたこれに対して同じテストを実行しましたが、実際に複製は1つだけです。Appleはarc4random()、例やドキュメントで他の乱数ジェネレーター(Swiftが引き継ぐ前など)の使用を促進してきたため、rand()プラットフォームのネイティブアプリでの使用はおそらくあまり一般的ではありません。
Arkku

私の質問に答える返信ありがとうございます。そして、(2 ^ 31)-2の期間は、私が観察したように、最後からすぐに繰り返される理由を説明しています。あなた(@ r3mainer)rand()は文書化されていないと述べましたが、@ Arkku は見かけのソースへのリンクを提供しています。私のシステムでそのファイルが見つからない理由、およびint rand(void) __swift_unavailable("Use arc4random instead.");Macでしか表示されない理由を知っていますstdlib.hか?@Arkkuにリンクされているコードがコンパイルされただけだと思います...どのライブラリですか?
セロンS

1
@TheronS Cライブラリlibcにコンパイルされ/usr/lib/libc.dylibます。=)
Arkku

5
どちらのバージョンrand()指定したCプログラムの使用「のコンパイラ」または「オペレーティングシステム」のではなく、C標準ライブラリの実装(例えば、によって決定されていませんglibclibc.dylibmsvcrt*.dll)。
ピーターO.

10

rand()はC標準で定義されており、C標準では使用するアルゴリズムが指定されていません。明らかに、AppleはGNU / Linuxの実装よりも劣ったアルゴリズムを使用しています。Linuxのアルゴリズムは、テストでの真のランダムソースと区別がつかないのに対し、Appleの実装は、数値をシャッフルするだけです。

任意の品質の乱数が必要な場合は、返される数値の品質に少なくともある程度の保証を与えるより優れたPRNGを使用するか、単純に読み取り/dev/urandomまたは同様の読み取りを行います。後者はあなたに暗号品質の数値を与えますが、遅いです。それ自体が遅すぎる場合でも、/dev/urandomいくつかの優れたシードを他のより高速なPRNGに提供できます。


返信いただきありがとうございます。私は実際には良いPRNGは必要ありません。ハッシュマップにいくつかの未定義の動作が潜んでいるのではないかと心配していましたが、その可能性を排除しても不思議に思いました。
セロンS

ところで、暗号で安全な乱数ジェネレータの例を次に示します。 github.com/divinity76/phpcpp/commit/... ..しかし、それは代わりにCのC ++だと私はSTLの実装は、すべての重労働を行うせるよ-
hanshenrik

3
@hanshenrik暗号RNGは一般的にやり過ぎであり、単純なハッシュテーブルには遅すぎます。
PM 2Ring

1
@ PM2Ringもちろんです。ハッシュテーブルハッシュは、主に高速である必要があります。ただし、高速であるだけでなくまともなハッシュテーブルアルゴリズムを開発したい場合は、暗号化ハッシュアルゴリズムのトリックのいくつかを知っておくとよいと思います。これは、最も高速なハッシュアルゴリズムをなぞる最も明白な間違いのほとんどを回避するのに役立ちます。それでも、ここでは特定の実装を宣伝しませんでした。
cmaster-モニカを回復させる

@cmaster十分だ。混合関数雪崩効果などについて少し知っておくことは確かに良い考えです。さいわい、xxhash、murmur3、siphashなど、速度をあまり犠牲にしない優れたプロパティを持つ非暗号ハッシュ関数があります。
PM 2Ring

5

一般に、rand / srandのペアは、結果の下位ビットが上位ビットよりもランダム性が低いため、長い間非推奨と見なされてきました。これは結果と関係がある場合とない場合がありますが、一部のrand / srand実装が最新になっている場合でも、古い実装が存続するため、random(3を使用することをお勧めします。 )。私のArch Linuxボックスでは、rand(3)のmanページに次のメモが残っています。

  The versions of rand() and srand() in the Linux C Library use the  same
   random number generator as random(3) and srandom(3), so the lower-order
   bits should be as random as the higher-order bits.  However,  on  older
   rand()  implementations,  and  on  current implementations on different
   systems, the lower-order bits are much less random than the  higher-or-
   der bits.  Do not use this function in applications intended to be por-
   table when good randomness is needed.  (Use random(3) instead.)

そのすぐ下のmanページには、実際には、randとsrandの非常に短くて非常に単純な実装例があり、これまでに見た中で最も単純なLC RNGであり、RAND_MAXが小さい。C標準ライブラリにあるものと一致するとは思わないでしょう。または、少なくとも私は望んでいない。

一般に、標準ライブラリから何かを使用する場合は、可能であればランダムに使用してください(manページには、POSIX.1-2001に戻るPOSIX標準としてリストされていますが、randは、Cが標準化される前の標準的な方法です) 。または、もっと良い方法として、数値レシピを開いて(またはオンラインで探して)、Knuthをクラックして実装します。それらは本当に簡単であり、あなたが最も頻繁に必要とする、既知の品質の属性を持つ汎用RNGを得るために、一度だけ実行する必要があります。


コンテキストをありがとう。私は実際には高品質のランダム性を必要とせず、MT19937を実装しましたが、Rustにあります。ほとんどの場合、2つのプラットフォームの動作が異なる理由を調べる方法に興味がありました。
セロンS

1
時には、厳密な必要性ではなく単純な関心から最良の質問が尋ねられることがあります。それらは、特定の好奇心の点から一連の良い答えを生むことが多いようです。あなたのものはそのうちの1つです。ここにすべての好奇心旺盛な人々、本当の、そして元のハッカーたちがいます。
Thomas Kammeyer

rand()を良くするのではなく、「rand()の使用をやめる」というアドバイスだったのはおかしいです。標準の中には、特定のジェネレータである必要があるとは書かれていません。
パイプ

2
@pipe rand()「より良い」にすることは、それを遅くすることを意味する場合(それはおそらく-暗号で保護された乱数は多くの労力を要します)、わずかに予測可能であっても、それを高速に保つことはおそらくより良いでしょう。適例:起動に時間がかかるプロダクションアプリケーションがあり、十分なエントロピーが生成されるまで待機する必要のある初期化を行ったRNGまでたどり着きました…それほど安全である必要はないことが判明したので、 「より悪い」RNGは大きな改善でした。
ギッズ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.