Yeni rasgele kitaplık neden std :: rand () 'dan daha iyi?


82

Bu yüzden rand () Zararlı Olarak Kabul Edildi adlı bir konuşma gördüm ve basit std::rand()artı modül paradigması üzerinden rastgele sayı üretiminin motor dağıtım paradigmasını kullanmayı savundu .

Ancak, başarısızlıkları std::rand()ilk elden görmek istedim, bu yüzden hızlı bir deney yaptım:

  1. Temel olarak, 2 işlev yazdım getRandNum_Old()ve getRandNum_New()bu, sırasıyla std::rand()ve std::mt19937+ kullanarak 0 ile 5 arasında rastgele bir sayı std::uniform_int_distributionürettim.
  2. Sonra "eski" yolu kullanarak 960.000 (6'ya bölünebilir) rastgele sayı ürettim ve 0-5 sayılarının frekanslarını kaydettim. Sonra bu frekansların standart sapmasını hesapladım. Aradığım şey, olabildiğince düşük bir standart sapma, çünkü dağılım gerçekten tekdüze olsaydı böyle olurdu.
  3. Bu simülasyonu 1000 kez çalıştırdım ve her simülasyon için standart sapmayı kaydettim. Ayrıca milisaniye cinsinden geçen zamanı da kaydettim.
  4. Daha sonra, aynısını tekrar yaptım ama bu sefer rastgele sayılar üretmek "yeni" yol.
  5. Son olarak, hem eski hem de yeni yol için standart sapmalar listesinin ortalamasını ve standart sapmasını ve hem eski hem de yeni yol için alınan zamanların listesi için ortalama ve standart sapmayı hesapladım.

Sonuçlar şunlardı:

[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

Şaşırtıcı bir şekilde, merdanelerin toplam yayılması her iki yöntem için de aynıydı. Yani, std::mt19937+ std::uniform_int_distributionbasit std::rand()+ 'dan "daha tek tip" değildi %. Yaptığım bir başka gözlem de yeninin eski yoldan yaklaşık 4 kat daha yavaş olduğuydu. Genel olarak, neredeyse hiç kalite kazanımı olmadan hız için çok büyük bir maliyet ödüyormuşum gibi görünüyordu.

Deneyim bir şekilde kusurlu mu? Yoksa std::rand()gerçekten o kadar da kötü değil ve belki daha da iyi mi?

Referans için, işte bütünüyle kullandığım kod:

#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));
}

32
Bu tavsiye büyük ölçüde bu yüzden var. RNG'yi yeterli entropi için nasıl test edeceğinizi veya programınız için önemli olup olmadığını bilmiyorsanız, std :: rand () 'ın yeterince iyi olmadığını varsaymalısınız. en.wikipedia.org/wiki/Entropy_(computing)
Hans Passant

4
rand()Yeterince iyi olup olmadığına dair alt satır, büyük ölçüde rastgele sayılar koleksiyonunu ne için kullandığınıza bağlıdır. Belirli bir rastgele dağıtım türüne ihtiyacınız varsa, tabii ki kütüphane uygulaması daha iyi olacaktır. Eğer sadece rastgele sayılara ihtiyacınız varsa ve "rastgelelik" ya da ne tür bir dağıtım üretildiğini umursamıyorsanız rand(), sorun değil. Elinizdeki işe uygun aleti eşleştirin.
David C. Rankin

2
olası dupe: stackoverflow.com/questions/52869166/… Bunu çekiçlemek istemiyorum, bu yüzden gerçekten oy vermekten kaçınıyorum.
bolov

18
for (i=0; i<k*n; i++) a[i]=i%n;piyasadaki en iyi RNG ile aynı tam ortalama ve standart sapmayı üretir. Bu uygulamanız için yeterince iyiyse, sadece bu sırayı kullanın.
n. zamirler 'm.

3
"mümkün olduğunca düşük standart sapma" - hayır. Bu yanlış. Sen beklemek sqrt (frekansı) hakkında size standart sapma olmasını beklediğiniz hakkındadır - frekanslar biraz farklı olması. Nm'nin ürettiği "artan sayaç" çok daha düşük bir sd'ye sahip olacaktır (ve çok kötü bir rng'dir).
Martin Bonner, Monica

Yanıtlar:


106

Hemen hemen her "eski" rand()uygulaması bir LCG kullanır ; Genellikle etraftaki en iyi üreticiler olmasalar da, genellikle bu kadar basit bir testte başarısız olduklarını görmezsiniz - ortalama ve standart sapma genellikle en kötü PRNG'ler tarafından bile doğru olur.

"Kötü" nin yaygın başarısızlıkları - ancak yeterince yaygın - rand()uygulamalar şunlardır:

  • düşük dereceli bitlerin düşük rastgeleliği;
  • kısa süre;
  • düşük RAND_MAX;
  • ardışık ekstraksiyonlar arasında bir miktar korelasyon (genel olarak, LCG'ler sınırlı sayıda hiper düzlemde sayılar üretir, ancak bu bir şekilde hafifletilebilir).

Yine de, bunların hiçbiri API'sine özgü değildir rand(). Belirli bir uygulama, bir xorshift ailesi oluşturucusunu srand/ randve algoritmik olarak konuşursak, arayüzde değişiklik olmaksızın son teknoloji ürünü bir PRNG elde edebilir, böylece yaptığınız gibi hiçbir test çıktıda herhangi bir zayıflık göstermez.

Düzenleme: @R. bu doğru notlar rand/ srandarayüz gerçeği ile sınırlıdır srandbir alır unsigned intde geride bir yürütme özünde sınırlıdır bir jeneratör, yani UINT_MAXolası bir başlangıç tohum (ve bu şekilde üretilen diziler). Bu gerçekten doğrudur, ancak API, srandalmak unsigned long longveya ayrı bir srand(unsigned char *, size_t)aşırı yükleme eklemek için önemsiz bir şekilde genişletilebilir .


Aslında, asıl sorun ilkerand() olarak uygulamayla ilgili değildir, ancak:

  • geriye dönük uyumluluk; mevcut uygulamaların çoğu, tipik olarak kötü seçilmiş parametrelerle, optimum altı üreteçleri kullanır; kötü şöhretli bir örnek, RAND_MAXyalnızca 32767 kullanan Visual C ++ 'dır . Bununla birlikte, geçmişle uyumluluğu bozacağı için bu kolayca değiştirilemez - srandtekrarlanabilir simülasyonlar için sabit bir tohumla kullanan insanlar çok mutlu olmaz (aslında, IIRC yukarıda bahsedilen uygulama seksenlerin ortalarından itibaren Microsoft C'nin önceki sürümlerine - hatta Lattice C'ye - geri dönüyor);
  • basit arayüz; rand()tüm program için küresel durumu tek bir jeneratör sağlar. Bu, birçok basit kullanım durumu için tamamen iyi (ve aslında oldukça kullanışlı) olsa da, problemler yaratır:

    • çok iş parçacıklı kod ile: düzeltmek için ya küresel bir mutekse ihtiyacınız vardır - bu, her şeyi sebepsiz yere yavaşlatır ve çağrıların sırası rastgele hale geldiği için tekrarlanabilirlik olasılığını ortadan kaldırır - ya da iş parçacığı yerel durumu; bu sonuncusu birkaç uygulama tarafından benimsenmiştir (özellikle Visual C ++);
    • programınızın küresel durumu etkilemeyen belirli bir modülüne "özel", yeniden üretilebilir bir dizi istiyorsanız.

Son olarak, randdurum:

  • gerçek bir uygulama belirtmez (C standardı yalnızca bir örnek uygulama sağlar), bu nedenle farklı derleyiciler arasında yeniden üretilebilir çıktı (veya bilinen kalitede bir PRNG bekleyen) üretmeyi amaçlayan herhangi bir program kendi oluşturucusunu döndürmelidir;
  • düzgün bir tohum elde etmek için herhangi bir çapraz platform yöntemi sağlamaz ( time(NULL)yeterince ayrıntılı olmadığı için değildir ve çoğu zaman - RTC'siz gömülü cihazları düşünün - yeterince rastgele bile değildir).

Bu nedenle, <random>bu karmaşayı düzeltmeye çalışan yeni başlık, şu algoritmaları sağlar:

  • tam olarak belirtilmiş (böylece çapraz derleyici tekrarlanabilir çıktı ve garantili özelliklere sahip olabilirsiniz - örneğin, jeneratör aralığı);
  • genel olarak en gelişmiş kalitede ( kütüphane tasarlandığı zamandan itibaren ; aşağıya bakınız);
  • sınıflar içinde kapsüllenmiştir (bu nedenle, iş parçacığı ve yerel olmayan sorunları tamamen ortadan kaldıran hiçbir küresel durum size zorlanmaz);

... ve random_deviceonları tohumlamak için de bir varsayılan .

Şimdi, bana sorarsanız , "kolay", "bir sayı tahmin et" durumları için bunun üzerine inşa edilmiş basit bir API de isterdim (Python'un "karmaşık" API'yi sağlaması gibi, aynı zamanda önemsiz random.randint& Co Rastgele cihazlarda / motorlarda / adaptörlerde / her ne zaman tombala kartları için bir sayı çıkarmak istediğimizde boğulmak istemeyen bizler için küresel, önceden tohumlanmış bir PRNG kullanarak), ancak kolayca yapabileceğiniz doğru. mevcut tesisler üzerinde kendiniz inşa edin ("tam" API'yi basit bir API üzerine inşa ederken mümkün olmayacaktır).


Son olarak, performans karşılaştırmanıza geri dönecek olursak: diğerlerinin de belirttiği gibi, hızlı bir LCG'yi daha yavaş (ancak genellikle daha kaliteli olduğu düşünülen) Mersenne Twister ile karşılaştırıyorsunuz; Bir LCG'nin kalitesinden memnunsanız, std::minstd_randbunun yerine kullanabilirsiniz std::mt19937.

Aslında, işlevinizi std::minstd_randbaşlatma için gereksiz statik değişkenleri kullanacak ve bunlardan kaçınacak şekilde ayarladıktan sonra

int getRandNum_New() {
    static std::minstd_rand eng{std::random_device{}()};
    static std::uniform_int_distribution<int> dist{0, 5};
    return dist(eng);
}

9 ms (eski) - 21 ms (yeni) alıyorum; son olarak, eğer kurtulursam dist(klasik modulo operatörüne kıyasla, çıkış aralığı için dağıtım eğriliğini giriş aralığının bir katı değil) ele alır ve ne yaptığınıza geri dönersemgetRandNum_Old()

int getRandNum_New() {
    static std::minstd_rand eng{std::random_device{}()};
    return eng() % 6;
}

Ben 6 ms için aşağı olsun (yani% 30 daha hızlı), çağrısına aksine muhtemelen, çünkü rand(), std::minstd_randsatır içi daha kolaydır.


Bu arada, aynı testi elle yuvarlanan (ancak standart kütüphane arayüzüne neredeyse uyumlu) kullanarak yaptım ve bu testten XorShift64*2,3 kat daha hızlı rand()(3,68 ms'ye karşı 8,61 ms); Mersenne Twister ve çeşitli sağlanan LCGs aksine, bu verilen uçan renklerle şimdiki rasgelelik test paketleri geçer ve blazingly hızlı, bunun henüz standart kütüphanede yer almayan neden meraklandırıyor.


3
Tam olarak belaya srandgiren belirsiz bir algoritmanın kombinasyonu std::rand. Ayrıca başka bir soruya verdiğim cevaba da bakınız .
Peter O.

2
randtohumun (ve dolayısıyla üretilebilecek olası dizilerin sayısının) sınırlandırılması açısından temelde API seviyesinde sınırlıdır UINT_MAX+1.
R .. GitHub BUZA YARDIM ETMEYİ DURDUR

2
sadece bir not: minstd kötü bir PRNG'dir, mt19973 daha iyidir ama fazla değil: pcg-random.org/… (bu çizelgede minstd == LCG32 / 64). C ++ 'nın PCG veya xoroshiro128 + gibi yüksek kaliteli, hızlı PRNG'ler sağlamaması oldukça utanç verici.
user60561

2
@MatteoItalia Anlaşmazlık içinde değiliz. Bu aynı zamanda Bjarne'nin noktasıydı. <random>Standardı gerçekten istiyoruz , ancak "bana şimdi kullanabileceğim düzgün bir uygulama ver" seçeneğini de istiyoruz. PRNG'ler ve diğer şeyler için.
ravnsgaard

2
Bazı notlar: 1. Değiştirme std::uniform_int_distribution<int> dist{0, 5}(eng);ile eng() % 6reintroduces eğriltme faktörü olduğunu std::rand(motor vardır bu durumda kuşkusuz minör eğrilik, kod uğrar 2**31 - 1çıkışları ve 6 kovalara dağıtmaya ediyoruz). 2. Yazıldığı gibi, olası çıktıları sınırlayan "bir srandalır unsigned int" hakkındaki notunuzda, motorunuzu tohumlamak std::random_device{}()da aynı soruna sahiptir; Eğer bir ihtiyaç seed_seqdüzgün ilklendir en PRNGs için .
ShadowRanger

6

Denemenizi 5'ten daha büyük bir aralıkla tekrarlarsanız, muhtemelen farklı sonuçlar göreceksiniz. Menziliniz önemli ölçüde daha küçük olduğunda RAND_MAX, çoğu uygulamada bir sorun yoktur.

Örneğin, bir RAND_MAX25'e rand() % 5sahipsek, aşağıdaki frekanslara sahip sayılar üreteceğiz:

0: 6
1: 5
2: 5
3: 5
4: 5

Olarak RAND_MAX32767'den fazla olmasını garanti ve en büyük olasılıkla, en büyük olasılıkla arasındaki frekanslarda farkıdır dağılımı çoğu kullanım durumları için rasgele yeterince yakın az sayıda için, sadece 1.


3
Bu
STL'nin

4
Tamam, ama ... STL kim? Ve hangi slaytlar? (ciddi soru)
kebs

@kebs, Stephan Lavavej, sorudaki Youtube referansına bakın.
Evg

3

İlk olarak, şaşırtıcı bir şekilde cevap, rastgele sayıyı ne için kullandığınıza bağlı olarak değişir. Rastgele bir arka plan rengi değiştirici sürmek için rand () kullanmak tamamen iyidir. Rastgele bir poker eli veya kriptografik olarak güvenli bir anahtar oluşturmak için rastgele bir sayı kullanıyorsanız, bu iyi değildir.

Tahmin edilebilirlik: 012345012345012345012345 ... dizisi, örneğinizdeki her sayının eşit bir dağılımını sağlar, ancak tabii ki rastgele değildir. Bir dizinin rastgele olması için, n + 1'in değeri, n'nin değeriyle (veya hatta n, n-1, n-2, n-3, vb. Değerleriyle) kolayca tahmin edilemez. Açıkça tekrar eden bir dizi aynı rakamlar dejenere bir durumdur, ancak herhangi bir doğrusal eşleşik üretici ile üretilen bir dizi analize tabi tutulabilir; Ortak bir kitaplıktan ortak bir LCG'nin varsayılan hazır ayarlarını kullanırsanız, kötü niyetli bir kişi çok fazla çaba harcamadan "sıralamayı bozabilir". Geçmişte, birkaç çevrimiçi kumarhane (ve bazı fiziksel kumarhaneler), zayıf rasgele sayı üreteçleri kullanan makinelerden zarar gördü. Daha iyi bilmesi gereken insanlar bile yakalandı;

Dağıtım: Videoda belirtildiği gibi, 100'lük bir modulo (veya dizinin uzunluğuna eşit olarak bölünemeyen herhangi bir değer) almak, bazı sonuçların diğer sonuçlardan en azından biraz daha olası hale geleceğini garanti edecektir. 32767 olası başlangıç ​​değerlerinin evreninde, modulo 100, 0'dan 66'ya kadar olan sayılar, 67'den 99'a kadar olan değerlerden 328/327 (% 0.3) daha sık görünecektir; bir saldırgana avantaj sağlayabilecek bir faktör.


1
"Tahmin edilebilirlik: 012345012345012345012345 ..." rastgelelik "için testinizi geçecektir, çünkü örneğinizdeki her sayının eşit bir dağılımı olacaktır" aslında, gerçekten değil; o ne ölçüyor stddev olan stddevs ait koşular arasında, yani çeşitli çalışır histogramı dışarı yayılır esasen nasıl. 012345012345012345 ... jeneratör ile her zaman sıfır olacaktır.
Matteo Italia

İyi bir nokta; Korkarım, OP'nin kodunu biraz fazla hızlı okudum. Cevabımı yansıtacak şekilde düzenledim.
JackLThornton

Hehe biliyorum çünkü o testi de yapmayı düşündüm ve farklı sonuçlar elde ettiğimi fark ettim 😄
Matteo Italia

1

Doğru cevap şudur: "daha iyi" ile ne demek istediğinize bağlıdır.

"Yeni" <random>motorlar 13 yıldan fazla bir süre önce C ++ 'ya tanıtıldı, bu yüzden gerçekten yeni değiller. C kütüphanesi rand()on yıllar önce tanıtıldı ve o dönemde pek çok şey için çok faydalı oldu.

C ++ standart kitaplığı üç sınıf rastgele sayı üreteci motoru sağlar: Doğrusal Eşlikli (bunun rand()bir örneğidir), Lagged Fibonacci ve Mersenne Twister. Her sınıfın değiş tokuşu vardır ve her sınıf belirli şekillerde "en iyisidir". Örneğin, LCG'lerin durumu çok küçüktür ve doğru parametreler seçilirse, modern masaüstü işlemcilerde oldukça hızlıdır. LFG'ler daha büyük bir duruma sahiptir ve yalnızca bellek getirme ve ekleme işlemini kullanır, bu nedenle özel matematik donanımı olmayan gömülü sistemler ve mikro denetleyicilerde çok hızlıdır. MTG devasa bir duruma sahiptir ve yavaştır, ancak mükemmel spektral özelliklere sahip çok büyük, tekrar etmeyen bir diziye sahip olabilir.

Sağlanan jeneratörlerin hiçbiri özel kullanımınız için yeterince iyi değilse, C ++ standart kitaplığı ayrıca bir donanım oluşturucu veya kendi özel motorunuz için bir arayüz sağlar. Üreticilerin hiçbirinin bağımsız olarak kullanılması amaçlanmamıştır: amaçlanan kullanımları, belirli bir olasılık dağılım fonksiyonu ile rastgele bir dizi sağlayan bir dağıtım nesnesi aracılığıyla gerçekleştirilir.

Bir başka avantajı, <random>üzerinde rand()yani rand(), kullanımları genel durumu evresel veya evre değildir ve işlem başına tek bir örneğini sağlar. Hassas bir kontrole veya öngörülebilirliğe ihtiyacınız varsa (yani, RNG tohum durumu verilen bir hatayı yeniden üretebiliyorsanız), o rand()zaman işe yaramaz. <random>Jeneratörler yerel instanced ve seri hale getirilebilir (ve restorable) devlet vardır.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.