Stworzyłem klasę QuickRandom
, której zadaniem jest szybkie tworzenie liczb losowych. To naprawdę proste: po prostu weź starą wartość, pomnóż przez a double
i weź część dziesiętną.
Oto moja QuickRandom
klasa w całości:
public class QuickRandom {
private double prevNum;
private double magicNumber;
public QuickRandom(double seed1, double seed2) {
if (seed1 >= 1 || seed1 < 0) throw new IllegalArgumentException("Seed 1 must be >= 0 and < 1, not " + seed1);
prevNum = seed1;
if (seed2 <= 1 || seed2 > 10) throw new IllegalArgumentException("Seed 2 must be > 1 and <= 10, not " + seed2);
magicNumber = seed2;
}
public QuickRandom() {
this(Math.random(), Math.random() * 10);
}
public double random() {
return prevNum = (prevNum*magicNumber)%1;
}
}
A oto kod, który napisałem, aby go przetestować:
public static void main(String[] args) {
QuickRandom qr = new QuickRandom();
/*for (int i = 0; i < 20; i ++) {
System.out.println(qr.random());
}*/
//Warm up
for (int i = 0; i < 10000000; i ++) {
Math.random();
qr.random();
System.nanoTime();
}
long oldTime;
oldTime = System.nanoTime();
for (int i = 0; i < 100000000; i ++) {
Math.random();
}
System.out.println(System.nanoTime() - oldTime);
oldTime = System.nanoTime();
for (int i = 0; i < 100000000; i ++) {
qr.random();
}
System.out.println(System.nanoTime() - oldTime);
}
Jest to bardzo prosty algorytm, który po prostu mnoży poprzednią wartość podwójną przez podwójną „magiczną liczbę”. Złożyłem to dość szybko, więc prawdopodobnie mógłbym to ulepszyć, ale dziwne, wydaje się, że działa dobrze.
Oto przykładowe dane wyjściowe zakomentowanych wierszy w main
metodzie:
0.612201846732229
0.5823974655091941
0.31062451498865684
0.8324473610354004
0.5907187526770246
0.38650264675748947
0.5243464344127049
0.7812828761272188
0.12417247811074805
0.1322738256858378
0.20614642573072284
0.8797579436677381
0.022122999476108518
0.2017298328387873
0.8394849894162446
0.6548917685640614
0.971667953190428
0.8602096647696964
0.8438709031160894
0.694884972852229
Hm. Całkiem losowo. W rzeczywistości działałoby to w przypadku generatora liczb losowych w grze.
Oto przykładowe dane wyjściowe części nieskomentowanej:
5456313909
1427223941
Łał! Działa prawie 4 razy szybciej niż Math.random
.
Pamiętam, że czytałem gdzieś, że Math.random
używano System.nanoTime()
i mnóstwo szalonych rzeczy dotyczących modułu i podziału. Czy to naprawdę konieczne? Mój algorytm działa znacznie szybciej i wydaje się dość przypadkowy.
Mam dwa pytania:
- Czy mój algorytm „wystarczająco dobre” (o, powiedzmy, na stadionie, gdzie naprawdę liczb losowych nie są zbyt ważne)?
- Dlaczego robi
Math.random
tak dużo, kiedy wydaje się, że wystarczy zwykłe pomnożenie i wycięcie ułamka dziesiętnego?
źródło
new QuickRandom(0,5)
lubnew QuickRandom(.5, 2)
. Oba będą wielokrotnie wyświetlać 0 dla twojego numeru.Odpowiedzi:
Twoja
QuickRandom
implementacja nie ma tak naprawdę jednolitej dystrybucji. Częstotliwości są generalnie wyższe przy niższych wartościach, przyMath.random()
bardziej równomiernym rozkładzie. Oto SSCCE, które pokazuje, że:Średni wynik wygląda następująco:
Jeśli powtórzysz test, zobaczysz, że rozkład QR różni się znacznie, w zależności od początkowych nasion, podczas gdy rozkład MR jest stabilny. Czasami osiąga pożądany jednolity rozkład, ale częściej nie. Oto jeden z bardziej ekstremalnych przykładów, nawet poza granicami wykresu:
źródło
QuickRandom
. Czasami jest zbliżony do munduru, czasami jest znacznie gorszy niż ten.To, co opisujesz, to rodzaj generatora losowego zwanego generatorem liniowym kongruencjalnym . Generator działa w następujący sposób:
Ten generator ma wiele fajnych właściwości, ale jako dobre źródło losowe ma poważne problemy. Powyższy artykuł w Wikipedii opisuje niektóre z mocnych i słabych stron. Krótko mówiąc, jeśli potrzebujesz dobrych wartości losowych, prawdopodobnie nie jest to zbyt dobre podejście.
Mam nadzieję że to pomoże!
źródło
Twoja funkcja liczb losowych jest słaba, ponieważ ma zbyt mały stan wewnętrzny - liczba wyjściowa funkcji w dowolnym kroku jest całkowicie zależna od poprzedniej liczby. Na przykład, jeśli przyjmiemy, że
magicNumber
jest to 2 (przykładowo), to sekwencja:jest silnie odzwierciedlone przez podobne sekwencje:
W wielu przypadkach spowoduje to zauważalne korelacje w Twojej grze - na przykład, jeśli wykonasz kolejne wywołania funkcji w celu wygenerowania współrzędnych X i Y dla obiektów, obiekty te utworzą wyraźne ukośne wzory.
Jeśli nie masz dobrego powodu, aby sądzić, że generator liczb losowych spowalnia twoją aplikację (a jest to BARDZO mało prawdopodobne), nie ma powodu, aby próbować pisać własny.
źródło
Prawdziwym problemem jest to, że jego wyjściowy histogram jest bardzo zależny od początkowego ziarna - przez większość czasu będzie to prawie jednolity wynik, ale przez większość czasu będzie miał wyraźnie niejednolity wynik.
Zainspirowany tym artykułem o tym, jak zła jest
rand()
funkcja php , stworzyłem kilka losowych obrazów macierzy przy użyciuQuickRandom
iSystem.Random
. Ten przebieg pokazuje, jak czasami ziarno może mieć zły wpływ (w tym przypadku faworyzując niższe liczby), gdySystem.Random
jest dość jednolite.QuickRandom
System.Random
Nawet gorzej
Jeśli mamy zainicjować
QuickRandom
jaknew QuickRandom(0.01, 1.03)
mamy do tego pliku:Kod
źródło
magicNumber
mnożenie daje liczbę podobną doprevNum
, co pokazuje brak losowości. Jeśli użyjemy nasionnew QuickRandom(0.01, 1.03)
, otrzymamy to i.imgur.com/Q1Yunbe.png !System.Drawing.Bitmap
.Jednym z problemów z generatorem liczb losowych jest to, że nie ma `` stanu ukrytego '' - jeśli wiem, jaką liczbę losową zwróciłeś podczas ostatniego połączenia, znam każdą liczbę losową, którą wyślesz do końca czasu, ponieważ jest tylko jedna możliwy następny wynik i tak dalej i tak dalej.
Inną rzeczą do rozważenia jest „okres” generatora liczb losowych. Oczywiście przy skończonym rozmiarze stanu, równym części mantysy podwójnej, będzie w stanie zwrócić najwyżej 2 ^ 52 wartości przed zapętleniem. Ale to w najlepszym przypadku - czy możesz udowodnić, że nie ma pętli okresu 1, 2, 3, 4 ...? Jeśli tak, twój RNG będzie miał w takich przypadkach okropne, zdegenerowane zachowanie.
Ponadto, czy generowanie liczb losowych będzie miało jednolity rozkład dla wszystkich punktów początkowych? Jeśli tak się nie stanie, Twój RNG będzie stronniczy - lub, co gorsza, na różne sposoby, w zależności od początkowego ziarna.
Jeśli potrafisz odpowiedzieć na wszystkie te pytania, super. Jeśli nie możesz, to wiesz, dlaczego większość ludzi nie wymyśla koła na nowo i używa sprawdzonego generatora liczb losowych;)
(Nawiasem mówiąc, dobrym porzekadłem jest: najszybszym kodem jest kod, który nie działa. Możesz zrobić najszybszą random () na świecie, ale nie jest dobrze, jeśli nie jest zbyt losowa)
źródło
0 -> 0
. W zależności od nasion może być wiele innych. (Na przykład, z nasion 3,00.5 -> 0.5
,0.25 -> 0.75 -> 0.25
,0.2 -> 0.6 -> 0.8 -> 0.4 -> 0.2
etc.)Jednym z powszechnych testów, które zawsze wykonywałem podczas tworzenia PRNG, było:
To pozwoliło mi szybko iterować pomysły, które były „wystarczająco dobre” PRNG dla sekwencji około 1 do 20 megabajtów. Dało to również lepszy obraz z góry na dół niż tylko oglądanie go na oko, ponieważ każdy „dostatecznie dobry” PRNG z półsłowiem stanu może szybko przekroczyć zdolność twoich oczu do zobaczenia punktu cyklu.
Gdybym był naprawdę wybredny, mógłbym wziąć dobre algorytmy i przeprowadzić na nich testy DIEHARD / NIST, aby uzyskać więcej wglądu, a następnie wrócić i trochę poprawić.
Zaletą testu kompresji, w przeciwieństwie do analizy częstotliwości, jest to, że w trywialny sposób łatwo jest zbudować dobry rozkład: po prostu wyślij blok o długości 256, zawierający wszystkie znaki o wartościach od 0 do 255, i zrób to 100 000 razy. Ale ta sekwencja ma cykl o długości 256.
Wypaczony rozkład, nawet z niewielkim marginesem, powinien zostać wychwycony przez algorytm kompresji, szczególnie jeśli dasz mu wystarczającą ilość (powiedzmy 1 megabajt) sekwencji do pracy. Jeśli niektóre znaki, bigramy lub n-gramów występują częściej, algorytm kompresji może zakodować to odchylenie dystrybucji do kodów, które faworyzują częste wystąpienia z krótszymi słowami kodowymi, a otrzymasz deltę kompresji.
Ponieważ większość algorytmów kompresji jest szybkich i nie wymagają implementacji (ponieważ systemy operacyjne mają je po prostu w pobliżu), test kompresji jest bardzo przydatny do szybkiego oceniania wyniku pozytywnego / negatywnego dla PRNG, który możesz tworzyć.
Powodzenia w eksperymentach!
Och, wykonałem ten test na rng, który masz powyżej, używając następującego małego moda twojego kodu:
Wyniki były następujące:
Uznałbym PRNG za dobry, gdyby plik wyjściowy nie mógł być w ogóle skompresowany. Szczerze mówiąc, nie sądziłem, że twój PRNG poradzi sobie tak dobrze, tylko 16% na ~ 20 Megs jest imponujące jak na tak prostą konstrukcję. Ale nadal uważam to za porażkę.
źródło
Najszybszym generatorem losowym, jaki możesz zaimplementować, jest:
XD, poza żartami, poza tym wszystkim, co tutaj zostało powiedziane, chciałbym wnieść swój wkład, powołując się na to, że testowanie losowych sekwencji „jest trudnym zadaniem” [1], a jest kilka testów, które sprawdzają pewne właściwości liczb pseudolosowych, można znaleźć wiele z nich tutaj: http://www.random.org/analysis/#2005
Jednym z prostych sposobów oceny „jakości” generatora losowego jest stary test Chi-kwadrat.
Cytując [1]
Korzystając z tej teorii i następującego kodu:
Otrzymałem następujący wynik:
Który w przypadku QuickRandom jest daleko od r (poza
r ± 2 * sqrt(r)
)To powiedziawszy, QuickRandom może być szybki, ale (jak stwierdzono w innych odpowiedziach) nie jest dobry jako generator liczb losowych
[1] SEDGEWICK ROBERT, Algorithms in C , Addinson Wesley Publishing Company, 1990, strony 516 do 518
źródło
int[]
są inicjalizowane na zero, więc nie ma potrzeby korzystania z tej części. Rzucanie na float jest bezcelowe, gdy pracujesz z podwójnymi. Na koniec: wywoływanie metod o nazwach random1 i random2 jest dość zabawne.Ułożyła szybki makiety swojego algorytmu w JavaScript, aby ocenić wyniki. Generuje 100 000 losowych liczb całkowitych od 0 do 99 i śledzi wystąpienie każdej liczby całkowitej.
Pierwszą rzeczą, jaką zauważyłem, jest to, że prawdopodobieństwo uzyskania niskiej liczby jest większe niż dużej. Widzisz to najbardziej, gdy
seed1
jest wysoki iseed2
niski. W kilku przypadkach uzyskałem tylko 3 liczby.W najlepszym przypadku twój algorytm wymaga dopracowania.
źródło
Jeśli
Math.Random()
funkcja wywołuje system operacyjny w celu uzyskania godziny, nie można jej porównać z funkcją. Twoja funkcja to PRNG, podczas gdy ta funkcja dąży do uzyskania prawdziwych liczb losowych. Jabłka i pomarańcze.Twój PRNG może być szybki, ale nie ma wystarczających informacji o stanie, aby osiągnąć długi okres, zanim się powtórzy (a jego logika nie jest wystarczająco wyrafinowana, aby osiągnąć nawet okresy, które są możliwe przy tak dużej ilości informacji o stanie).
Okres to długość sekwencji, zanim PRNG zacznie się powtarzać. Dzieje się tak, gdy tylko maszyna PRNG przechodzi do stanu, który jest identyczny z jakimś poprzednim stanem. Stamtąd powtórzy przejścia, które rozpoczęły się w tym stanie. Innym problemem związanym z PRNG może być mała liczba unikalnych sekwencji, a także zdegenerowana zbieżność w konkretnej sekwencji, która się powtarza. Mogą również występować niepożądane wzory. Na przykład przypuśćmy, że PRNG wygląda dość losowo, gdy liczby są drukowane w postaci dziesiętnej, ale sprawdzenie wartości w systemie binarnym pokazuje, że bit 4 po prostu przełącza się między 0 a 1 przy każdym wywołaniu. Ups!
Spójrz na Mersenne Twister i inne algorytmy. Istnieją sposoby na osiągnięcie równowagi między długością okresu a cyklami procesora. Jedną z podstawowych metod (stosowanych w Mersenne Twister) jest cykliczne poruszanie się po wektorze stanu. Oznacza to, że kiedy generowana jest liczba, nie jest ona oparta na całym stanie, tylko na kilku słowach z tablicy stanów podlegających kilku operacjom bitowym. Ale na każdym kroku algorytm porusza się również po tablicy, po trochu szyfrując zawartość.
źródło
/dev/random
jest źródłem rzeczywistej losowości uzyskanej ze sterowników urządzeń, a nie PRNG. Blokuje się, gdy nie ma wystarczającej liczby bitów. Siostrzane urządzenie/dev/urandom
również nie blokuje, ale nadal nie jest dokładnie PRNG, ponieważ jest aktualizowane losowymi bitami, gdy są dostępne.nanoTime()
+ licznik / hash są używane jako domyślne ziarnojava.util.Random
oracle / OpenJDK. To tylko dla nasion, wtedy jest to standardowe LCG. W efekcie generator OP pobiera 2 liczby losowe jako ziarno, co jest w porządku - więc nie ma różnicy niżjava.util.Random
.System.currentTimeMillis()
był domyślnym ziarnem w JDK1.4-Istnieje wiele, wiele generatorów liczb pseudolosowych. Na przykład ranarray Knutha , twister Mersenne lub poszukaj generatorów LFSR. Monumentalne "algorytmy seminumeryczne" Knutha analizują ten obszar i proponują pewne liniowe generatory kongruencjalne (proste w implementacji, szybkie).
Ale sugerowałbym, abyś po prostu trzymał się
java.util.Random
lubMath.random
, są one szybkie i przynajmniej OK do sporadycznego użytku (np. Gry i tym podobne). Jeśli jesteś paranoikiem w dystrybucji (jakiś program Monte Carlo lub algorytm genetyczny), sprawdź ich implementację (źródło jest gdzieś dostępne) i zasyp je jakąś naprawdę losową liczbą, albo z twojego systemu operacyjnego, albo z random.org . Jeśli jest to wymagane w przypadku aplikacji, w których bezpieczeństwo ma kluczowe znaczenie, będziesz musiał się kopać. I tak jak w takim przypadku nie powinniście wierzyć w to, co wylewa tutaj jakiś kolorowy kwadrat z brakującymi kawałkami, zamknę się teraz.źródło
Jest bardzo mało prawdopodobne, aby wydajność generowania liczb losowych była problemem w każdym przypadku użycia, który wymyśliłeś, chyba że uzyskujesz dostęp do pojedynczej
Random
instancji z wielu wątków (ponieważRandom
jestsynchronized
).Jeśli jednak tak jest naprawdę i potrzebujesz szybko wielu liczb losowych, Twoje rozwiązanie jest zbyt niewiarygodne. Czasami daje dobre rezultaty, czasami daje okropne rezultaty (na podstawie początkowych ustawień).
Jeśli chcesz uzyskać te same liczby, które
Random
daje ci klasa, tylko szybciej, możesz pozbyć się tam synchronizacji:Po prostu wziąłem
java.util.Random
kod i usunąłem synchronizację, co daje dwukrotnie wyższą wydajność w porównaniu z oryginałem na mojej Oracle HotSpot JVM 7u9. Nadal jest wolniejszy niż twójQuickRandom
, ale daje znacznie bardziej spójne wyniki. Aby być precyzyjnym, dla tych samychseed
wartości i aplikacji jednowątkowych daje te same liczby pseudolosowe, co oryginalnaRandom
klasa.Ten kod jest oparty na aktualnej wersji
java.util.Random
OpenJDK 7u, która jest licencjonowana w ramach GNU GPL v2 .EDYCJA 10 miesięcy później:
Właśnie odkryłem, że nie musisz nawet używać mojego kodu powyżej, aby uzyskać niezsynchronizowaną
Random
instancję. Jest też jeden w JDK!Spójrz na
ThreadLocalRandom
klasę Java 7 . Kod wewnątrz jest prawie identyczny z moim kodem powyżej. Klasa jest po prostuRandom
wersją izolowaną przez lokalne wątki, odpowiednią do szybkiego generowania liczb losowych. Jedynym minusem, jaki przychodzi mi do głowy, jest to, że nie możesz ustawić goseed
ręcznie.Przykładowe użycie:
źródło
:)
To ciekawe, dzięki!„Losowe” to coś więcej niż tylko zdobywanie liczb… to, co masz, jest pseudolosowe
Jeśli pseudolosowość jest wystarczająco dobra do twoich celów, to na pewno jest o wiele szybsza (a XOR + Bitshift będzie szybszy niż to, co masz)
Rolf
Edytować:
OK, po zbyt pochopnym udzieleniu odpowiedzi, pozwól mi odpowiedzieć na prawdziwy powód, dla którego twój kod jest szybszy:
Z JavaDoc dla Math.Random ()
Prawdopodobnie dlatego twój kod jest szybszy.
źródło
Math.random()
jest wolniejsza. Musiałby albo zaRandom
każdym razem synchronizować, albo tworzyć nowe , a żaden z nich nie byłby zbyt atrakcyjny pod względem wydajności. Gdybym dbał o wydajność, stworzyłbym własnąnew Random
i po prostu użyłbym tego. : Pjava.util.Random niewiele różni się od podstawowego LCG opisanego przez Knutha. Jednak ma główne 2 główne zalety / różnice:
Poniżej znajduje się główna procedura generująca „losowe” liczby całkowite w java.util.Random.
Jeśli usuniesz AtomicLong i nieujawnioną wartość (tj. Używając wszystkich bitów
long
), uzyskasz większą wydajność niż podwójne mnożenie / modulo.Ostatnia uwaga:
Math.random
nie powinien być używany do niczego poza prostymi testami, jest podatny na spory, a jeśli masz nawet kilka wątków wywołujących go jednocześnie, wydajność spada. Jedną mało znaną historyczną cechą tego rozwiązania jest wprowadzenie CAS w Javie - aby pokonać niesławny test porównawczy (najpierw przez IBM przez intrinsics, a potem Sun stworzył "CAS z Java")źródło
To jest funkcja losowa, której używam w moich grach. Jest dość szybki i ma dobrą (wystarczającą) dystrybucję.
źródło
volatile
niej kompilator może dowolnie eliminować (lub wprowadzać) zmienne lokalne.