Це "достатньо хороший" випадковий алгоритм; чому він не використовується, якщо він швидший?


171

Я склав клас під назвою QuickRandom, і його завдання полягає в швидкому формуванні випадкових чисел. Це дійсно просто: просто візьміть старе значення, помножте на a doubleі візьміть десяткову частину.

Ось мій QuickRandomклас у всій повноті:

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

}

А ось код, який я написав, щоб перевірити його:

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

Це дуже простий алгоритм, який просто множить попередній подвійний на подвійне "магічне число". Я зібрав це досить швидко, так що, напевно, я міг би зробити це краще, але як не дивно, здається, це працює нормально.

Це зразок виводу коментованих рядків mainметоду:

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

Гм. Досить випадковий. Насправді, це працювало б для генератора випадкових чисел у грі.

Ось зразок виводу частини, що не коментується:

5456313909
1427223941

Оце Так! Він виконує майже в 4 рази швидше, ніж Math.random.

Я пам’ятаю, що десь читав, що Math.randomвикористовував System.nanoTime()і тонни шаленого модуля та матеріалів поділу. Це справді потрібно? Мій алгоритм працює набагато швидше і здається досить випадковим.

У мене є два питання:

  • Чи мій алгоритм "достатньо хороший" (наприклад, для гри, де дійсно випадкові числа не надто важливі)?
  • Чому Math.randomтак багато робиться, коли здається, що просто просто множення і вирізання десятків буде достатньо?

154
"здається досить випадковим"; ви повинні генерувати гістограму та запустити деяку автокореляцію у своїй послідовності ...
Олівер Чарльворт,

63
Він означає, що "здається досить випадковим" насправді не є об'єктивним показником випадковості, і ви повинні отримати фактичну статистику.
Метт H

23
@Doorknob: З точки зору непростої людини, ви повинні дослідити, чи має ваші номери "плоский" розподіл між 0 і 1, і побачити, чи існують періодичні / повторювані шаблони з часом.
Олівер Чарльворт

22
Спробуйте new QuickRandom(0,5)або new QuickRandom(.5, 2). Вони будуть неодноразово виводити 0 для вашого номера.
FrankieTheKneeMan

119
Написання власного алгоритму генерації випадкових чисел - це як написання власного алгоритму шифрування. Людей, які мають гіперкваліфікацію, існує стільки відомого рівня техніки, що безглуздо витрачати свій час, намагаючись виправити це. Немає причин не використовувати функції бібліотеки Java, і якщо ви дійсно хочете написати чомусь своє, відвідайте Вікіпедію та знайдіть там такі алгоритми, як Mersenne Twister.
steveha

Відповіді:


351

Ваша QuickRandomреалізація насправді не має рівномірного розподілу. Частоти, як правило, вище при нижчих значеннях, тоді як Math.random()мають більш рівномірний розподіл. Ось SSCCE, який показує, що:

package com.stackoverflow.q14491966;

import java.util.Arrays;

public class Test {

    public static void main(String[] args) throws Exception {
        QuickRandom qr = new QuickRandom();
        int[] frequencies = new int[10];
        for (int i = 0; i < 100000; i++) {
            frequencies[(int) (qr.random() * 10)]++;
        }
        printDistribution("QR", frequencies);

        frequencies = new int[10];
        for (int i = 0; i < 100000; i++) {
            frequencies[(int) (Math.random() * 10)]++;
        }
        printDistribution("MR", frequencies);
    }

    public static void printDistribution(String name, int[] frequencies) {
        System.out.printf("%n%s distribution |8000     |9000     |10000    |11000    |12000%n", name);
        for (int i = 0; i < 10; i++) {
            char[] bar = "                                                  ".toCharArray(); // 50 chars.
            Arrays.fill(bar, 0, Math.max(0, Math.min(50, frequencies[i] / 100 - 80)), '#');
            System.out.printf("0.%dxxx: %6d  :%s%n", i, frequencies[i], new String(bar));
        }
    }

}

Середній результат виглядає приблизно так:

QR distribution |8000     |9000     |10000    |11000    |12000
0.0xxx:  11376  :#################################                 
0.1xxx:  11178  :###############################                   
0.2xxx:  11312  :#################################                 
0.3xxx:  10809  :############################                      
0.4xxx:  10242  :######################                            
0.5xxx:   8860  :########                                          
0.6xxx:   9004  :##########                                        
0.7xxx:   8987  :#########                                         
0.8xxx:   9075  :##########                                        
0.9xxx:   9157  :###########                                       

MR distribution |8000     |9000     |10000    |11000    |12000
0.0xxx:  10097  :####################                              
0.1xxx:   9901  :###################                               
0.2xxx:  10018  :####################                              
0.3xxx:   9956  :###################                               
0.4xxx:   9974  :###################                               
0.5xxx:  10007  :####################                              
0.6xxx:  10136  :#####################                             
0.7xxx:   9937  :###################                               
0.8xxx:  10029  :####################                              
0.9xxx:   9945  :###################    

Якщо ви повторите тест, ви побачите, що розподіл QR сильно змінюється, залежно від початкових насінин, тоді як розподіл MR є стабільним. Іноді він досягає бажаного рівномірного розподілу, але частіше за все це не дає. Ось один із найбільш екстремальних прикладів, це навіть поза межами графіка:

QR distribution |8000     |9000     |10000    |11000    |12000
0.0xxx:  41788  :##################################################
0.1xxx:  17495  :##################################################
0.2xxx:  10285  :######################                            
0.3xxx:   7273  :                                                  
0.4xxx:   5643  :                                                  
0.5xxx:   4608  :                                                  
0.6xxx:   3907  :                                                  
0.7xxx:   3350  :                                                  
0.8xxx:   2999  :                                                  
0.9xxx:   2652  :                                                  

17
+1 для числових даних, хоча перегляд вихідних цифр може ввести в оману, оскільки це не означає, що вони мають статистично значущі відмінності.
Maciej Piechotka

16
Ці результати сильно відрізняються від початкового насіння, переданого до QuickRandom. Іноді він близький до рівномірного, іноді набагато гірший за це.
Петро Янечек

68
@ BlueRaja-DannyPflughoeft Будь-який PRNG, де якість вироблення сильно залежить від початкових значень насіння (на відміну від внутрішніх констант), мені здається порушеним.
CVn

22
Перше правило статистики: побудувати дані . Ваш аналіз точковий, але побудова гістограми показує це набагато швидше. ;-) (І це два рядки в Р.)
Конрад Рудольф

37
Обов’язкові цитати: "Кожен, хто розглядає арифметичні методи отримання випадкових цифр, звичайно, перебуває у гріху". - Джон фон Нойман (1951) "Той, хто не бачив вищезазначеної цитати хоча б в 100 місцях, напевно, не дуже старий". - DV Pryor (1993) "Генератори випадкових чисел не слід вибирати випадково". - Дональд Кнут (1986)
Happy Green Kid Naps

133

Те, що ви описуєте, - це тип випадкового генератора, який називається лінійним конгрурентним генератором . Генератор працює наступним чином:

  • Почніть зі значення насіння та мультиплікатора.
  • Щоб створити випадкове число:
    • Помножте насіння множником.
    • Встановіть насіння рівне цій величині.
    • Повернути це значення.

Цей генератор має багато приємних властивостей, але має значні проблеми як хороший випадковий джерело. Згадана вище стаття у Вікіпедії описує деякі сильні та слабкі сторони. Коротше кажучи, якщо вам потрібні хороші випадкові значення, це, мабуть, не дуже хороший підхід.

Сподіваюся, це допомагає!


@ louism - Це насправді не "випадково". Результати будуть детермінованими. Це сказало, що я не думав про це, коли писав свою відповідь; можливо, хтось може уточнити цю деталь?
templatetypedef

2
Арифметичні помилки з плаваючою комою розроблені для реалізації. Наскільки мені відомо, вони відповідають певній платформі, але можуть відрізнятися, наприклад, між різними мобільними телефонами та між архітектурами ПК. Хоча іноді додаються додаткові «захисні біти», коли виконуються ряд обчислень з плаваючою комою поспіль, і наявність або відсутність цих захисних бітів може зробити обчислення тонко різними в результаті. (біт охоронця, наприклад, розширення 64-бітового подвійного до 80 біт)
Паташу,

2
Також майте на увазі, що теорія, що стоїть за LCRNG, передбачає, що ви працюєте з цілими числами! Кидання на нього чисел з плаваючою комою не дасть такої ж якості результатів.
duskwuff -inactive-

1
@duskwuff, ти маєш рацію. Але якщо апаратне забезпечення з плаваючою комою дотримується розумних правил, це робиться так само, як робити це за модулем розмір мантіси, і теорія застосовується. Просто потрібен додатковий догляд у тому, що ви робите.
фонбранд

113

Ваша функція випадкових чисел погана, оскільки має надто мало внутрішнього стану - число, яке виводиться функцією на будь-якому етапі, повністю залежить від попереднього числа. Наприклад, якщо припустити, що magicNumberце 2 (як приклад), то послідовність:

0.10 -> 0.20

сильно відображено подібними послідовностями:

0.09 -> 0.18
0.11 -> 0.22

У багатьох випадках це призведе до помітних кореляцій у вашій грі - наприклад, якщо ви робите послідовні дзвінки у свою функцію, щоб генерувати X та Y координати для об'єктів, об'єкти формуватимуть чіткі діагональні структури.

Якщо у вас немає вагомих причин вважати, що генератор випадкових чисел уповільнює вашу програму (а це ДУЖЕ малоймовірно), немає жодних вагомих причин спробувати написати своє.


36
+1 для практичної відповіді ... використовуйте це для того, щоб вистрілити і породити ворогів уздовж діагоналей для епічних кількох головних ударів? : D
вім

@wim: вам не потрібен PRNG, якщо ви хочете таких моделей.
Лежи Райан

109

Справжня проблема в цьому полягає в тому, що його гістограма на виході значною мірою залежить від початкового насіння - значна частина часу закінчиться майже рівномірним результатом, але багато часу матиме чітко нерівномірний вихід.

Натхненний цією статтею про те, наскільки погана rand()функція php , я створив кілька випадкових матричних зображень за допомогою QuickRandomі System.Random. Цей пробіг показує, як іноді насіння може мати поганий ефект (у цьому випадку перевагу менших чисел), де System.Randomце досить рівномірно.

QuickRandom

System.Random

Навіть гірше

Якщо ми ініціалізуємо, QuickRandomяк new QuickRandom(0.01, 1.03)отримуємо це зображення:

Код

using System;
using System.Drawing;
using System.Drawing.Imaging;

namespace QuickRandomTest
{
    public class QuickRandom
    {
        private double prevNum;
        private readonly double magicNumber;

        private static readonly Random rand = new Random();

        public QuickRandom(double seed1, double seed2)
        {
            if (seed1 >= 1 || seed1 < 0) throw new ArgumentException("Seed 1 must be >= 0 and < 1, not " + seed1);
            prevNum = seed1;
            if (seed2 <= 1 || seed2 > 10) throw new ArgumentException("Seed 2 must be > 1 and <= 10, not " + seed2);
            magicNumber = seed2;
        }

        public QuickRandom()
            : this(rand.NextDouble(), rand.NextDouble() * 10)
        {
        }

        public double Random()
        {
            return prevNum = (prevNum * magicNumber) % 1;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            var rand = new Random();
            var qrand = new QuickRandom();
            int w = 600;
            int h = 600;
            CreateMatrix(w, h, rand.NextDouble).Save("System.Random.png", ImageFormat.Png);
            CreateMatrix(w, h, qrand.Random).Save("QuickRandom.png", ImageFormat.Png);
        }

        private static Image CreateMatrix(int width, int height, Func<double> f)
        {
            var bitmap = new Bitmap(width, height);
            for (int y = 0; y < height; y++) {
                for (int x = 0; x < width; x++) {
                    var c = (int) (f()*255);
                    bitmap.SetPixel(x, y, Color.FromArgb(c,c,c));
                }
            }

            return bitmap;
        }
    }
}

2
Хороший код. Так, це класно. Раніше я це робив занадто інколи, важко отримати з нього кількісно вимірювану міру, але це ще один хороший спосіб переглянути послідовність. І якби ви хотіли переглянути послідовності, що перевищують ширину * висоту, ви можете зібрати наступне зображення цим пікселем на піксель. Я думаю, що картина QuickRandom набагато естетичніше, хоча вона має текстуру, як килим морських водоростей.
Cris Stringfellow

Естетично приваблива частина полягає в тому, як послідовність має тенденцію до збільшення, коли ви йдете по кожному ряду (а потім знову до початку), оскільки magicNumberмноження виробляє число, подібне до prevNum, що свідчить про відсутність випадковості. Якщо ми використовуємо насіння, new QuickRandom(0.01, 1.03)то отримаємо цей i.imgur.com/Q1Yunbe.png !
Callum Rogers

Так, чудовий аналіз. Оскільки він просто помножує mod 1 на постійну чітко перед тим, як загортати, буде описане вами збільшення. Схоже, цього можна уникнути, якби ми взяли менш значні десяткові розміщення, скажімо, помноживши на 1 мільярд і зменшивши мод на 256 кольорову палітру.
Cris Stringfellow

Чи можете ви сказати мені, що ви використовували для створення цих вихідних зображень? Матлаб?
удень

@uDaY: Погляньте на код, C # і System.Drawing.Bitmap.
Каллум Роджерс

37

Одна проблема у вашому генераторі випадкових чисел полягає в тому, що немає "прихованого стану" - якщо я знаю, яке випадкове число ви повернулися під час останнього дзвінка, я знаю кожне випадкове число, яке ви надішлете до кінця часу, оскільки існує лише одне можливий наступний результат тощо і так далі.

Інша річ, яку слід врахувати, - це "період" вашого генератора випадкових чисел. Очевидно, що з кінцевим розміром стану, рівним частині мантіси в подвійному, він зможе повернути щонайбільше 2 ^ 52 значення перед циклом. Але це в кращому випадку - чи можете ви довести, що немає циклів періоду 1, 2, 3, 4 ...? Якщо вони є, ваш RNG матиме жахливу, вироджену поведінку в цих випадках.

Крім того, чи матиме ваше генерація випадкових чисел рівномірний розподіл для всіх вихідних точок? Якщо цього не відбувається, то ваш RNG буде упередженим - або ще гірше, упередженим по-різному залежно від стартового насіння.

Якщо ви зможете відповісти на всі ці питання, приголомшливо. Якщо ви не можете, то знаєте, чому більшість людей не винаходять колесо і використовують перевірений генератор випадкових чисел;)

(До речі, хороша приказка: найшвидший код - це код, який не працює. Ви можете зробити найшвидший випадковий () у світі, але це не добре, якщо він не дуже випадковий)


8
Існує принаймні один тривіальний цикл на цьому генераторі для всіх насіння 0 -> 0. Залежно від насіння може бути багато інших. (Так , наприклад, з насінням 3,0, 0.5 -> 0.5, 0.25 -> 0.75 -> 0.25, 0.2 -> 0.6 -> 0.8 -> 0.4 -> 0.2і т.д.)
duskwuff -inactive-

36

Одним із загальних тестів, які я завжди робив при розробці PRNG, було:

  1. Перетворити вихід у значення знаків
  2. Запишіть значення символу у файл
  3. Стиснути файл

Це дозволило мені швидко проаналізувати ідеї, які були «досить хорошими» PRNG для послідовностей приблизно від 1 до 20 мегабайт. Це також дало кращу картину зверху вниз, ніж просто оглядати її оком, оскільки будь-який «досить хороший» PRNG із станом півслова стану може швидко перевищити здатність ваших очей бачити точку циклу.

Якби я був справді вибагливий, я міг би взяти хороші алгоритми і запустити на них тести DIEHARD / NIST, щоб отримати більше уявлення, а потім повернутися назад і змінити ще трохи.

Перевага тесту на стиснення, на відміну від частотного аналізу, полягає в тому, що тривіально легко побудувати хороший розподіл: просто виведіть блок довжиною 256, що містить усі знаки значень 0 - 255, і зробіть це 100 000 разів. Але ця послідовність має цикл довжиною 256.

Перекошений розподіл, навіть з невеликим запасом, повинен вибиратися алгоритмом стиснення, особливо якщо ви даєте йому достатню кількість (скажімо, 1 мегабайт) послідовності для роботи. Якщо деякі символи, або діаграми, або n-грами зустрічаються частіше, алгоритм стиснення може кодувати цей перекид розподілу до кодів, які сприяють частому виникненню з коротшими кодовими словами, і ви отримуєте дельту стиснення.

Оскільки більшість алгоритмів стиснення швидкі, і вони не потребують впровадження (оскільки в ОС їх просто лежить), тест на стиснення є дуже корисним для швидкого оцінювання проходження / відмови для PRNG, який ви можете розробляти.

Успіхів у ваших експериментах!

О, я провів цей тест на rng, який у вас є вище, використовуючи наступний невеликий мод вашого коду:

import java.io.*;

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

    public static void main(String[] args) throws Exception {
        QuickRandom qr = new QuickRandom();
        FileOutputStream fout = new FileOutputStream("qr20M.bin");

        for (int i = 0; i < 20000000; i ++) {
            fout.write((char)(qr.random()*256));
        }
    }
}

Результати:

Cris-Mac-Book-2:rt cris$ zip -9 qr20M.zip qr20M.bin2
adding: qr20M.bin2 (deflated 16%)
Cris-Mac-Book-2:rt cris$ ls -al
total 104400
drwxr-xr-x   8 cris  staff       272 Jan 25 05:09 .
drwxr-xr-x+ 48 cris  staff      1632 Jan 25 05:04 ..
-rw-r--r--   1 cris  staff      1243 Jan 25 04:54 QuickRandom.class
-rw-r--r--   1 cris  staff       883 Jan 25 05:04 QuickRandom.java
-rw-r--r--   1 cris  staff  16717260 Jan 25 04:55 qr20M.bin.gz
-rw-r--r--   1 cris  staff  20000000 Jan 25 05:07 qr20M.bin2
-rw-r--r--   1 cris  staff  16717402 Jan 25 05:09 qr20M.zip

Я б вважав PRNG хорошим, якщо вихідний файл не вдалося б стиснути взагалі. Якщо чесно кажучи, я не думав, що ваш PRNG зробить це так добре, лише 16% на ~ 20 мег досить вражаючі для такої простої конструкції. Але я все-таки вважаю це провалом.


2
Уявляючи це чи ні, я маю таку ж ідею з блискавкою років тому, коли я тестую свої випадкові генератори.
Арістос

1
Дякую @Alexandre C. та Aristos та Assanan. Я тобі вірю.
Cris Stringfellow

33

Найшвидший випадковий генератор, який ви могли реалізувати:

введіть тут опис зображення

XD, жартуємо окремо, окрім усього сказаного тут, я б хотів зробити свій внесок, посилаючись на те, що тестування випадкових послідовностей "- важка задача" [1], і є кілька тестів, які перевіряють певні властивості псевдовипадкових чисел, ви можете знайти їх тут багато: http://www.random.org/analysis/#2005

Один з простих способів оцінити "якість" випадкових генераторів - це старий тест Чі Квадрат.

static double chisquare(int numberCount, int maxRandomNumber) {
    long[] f = new long[maxRandomNumber];
    for (long i = 0; i < numberCount; i++) {
        f[randomint(maxRandomNumber)]++;
    }

    long t = 0;
    for (int i = 0; i < maxRandomNumber; i++) {
        t += f[i] * f[i];
    }
    return (((double) maxRandomNumber * t) / numberCount) - (double) (numberCount);
}

Цитуючи [1]

Ідея χ²-тесту полягає у тому, щоб перевірити, чи отримано чисельність розумно розподілених чи ні. Якщо ми генеруємо N позитивних чисел менше r , то ми очікуємо отримати приблизно N / r чисел кожного значення. Але --- і в цьому суть справи --- частоти виникнення всіх значень не повинні бути абсолютно однаковими: це не було б випадково!

Ми просто обчислюємо суму квадратів частот виникнення кожного значення, масштабується на очікувану частоту, а потім субстратуємо від розміру послідовності. Це число, "χ² статистика", може бути виражене математично як

формула чі в квадраті

Якщо статистика χ² близька до r , то числа випадкові; якщо це занадто далеко, то їх немає. Поняття «близько» та «далеко» можна точніше визначити: існують таблиці, які точно розповідають про те, наскільки співвідносять статистику з властивостями випадкових послідовностей. Для простого тесту, який ми виконуємо, статистика повинна бути в межах 2√r

Використовуючи цю теорію та наступний код:

abstract class RandomFunction {
    public abstract int randomint(int range); 
}

public class test {
    static QuickRandom qr = new QuickRandom();

    static double chisquare(int numberCount, int maxRandomNumber, RandomFunction function) {
        long[] f = new long[maxRandomNumber];
        for (long i = 0; i < numberCount; i++) {
            f[function.randomint(maxRandomNumber)]++;
        }

        long t = 0;
        for (int i = 0; i < maxRandomNumber; i++) {
            t += f[i] * f[i];
        }
        return (((double) maxRandomNumber * t) / numberCount) - (double) (numberCount);
    }

    public static void main(String[] args) {
        final int ITERATION_COUNT = 1000;
        final int N = 5000000;
        final int R = 100000;

        double total = 0.0;
        RandomFunction qrRandomInt = new RandomFunction() {
            @Override
            public int randomint(int range) {
                return (int) (qr.random() * range);
            }
        }; 
        for (int i = 0; i < ITERATION_COUNT; i++) {
            total += chisquare(N, R, qrRandomInt);
        }
        System.out.printf("Ave Chi2 for QR: %f \n", total / ITERATION_COUNT);        

        total = 0.0;
        RandomFunction mathRandomInt = new RandomFunction() {
            @Override
            public int randomint(int range) {
                return (int) (Math.random() * range);
            }
        };         
        for (int i = 0; i < ITERATION_COUNT; i++) {
            total += chisquare(N, R, mathRandomInt);
        }
        System.out.printf("Ave Chi2 for Math.random: %f \n", total / ITERATION_COUNT);
    }
}

Я отримав такий результат:

Ave Chi2 for QR: 108965,078640
Ave Chi2 for Math.random: 99988,629040

Що, для QuickRandom, далеко від r (поза r ± 2 * sqrt(r))

Як було сказано, QuickRandom може бути швидким, але (як зазначено в інших відповідях) не добре, як генератор випадкових чисел


[1] SEDGEWICK ROBERT, Алгоритми C , Видавництво Аддінсона Веслі, 1990, стор. 516 до 518


9
+1 для xkcd, це чудовий веб-сайт (о, і чудова відповідь): P
tckmn

1
Дякую, і так, xkcd стелажі! XD
higuaro

Теорія хороша, але виконання є поганим: код сприйнятливий до цілого переповнення. У Java всі int[]ініціалізовані до нуля, тому немає необхідності в цій частині. Кастинг в плавати безглуздий, коли ви працюєте з парними. Останнє: називати імена методів random1 та random2 досить смішно.
bestsss

@bestsss Дякую за спостереження! Я зробив прямий переклад з коду С і не звертав на нього великої уваги = (. Я вніс деякі зміни та оновив відповідь. Буду вдячний за будь-яку додаткову пропозицію
higuaro

14

Я зібрав швидкий макет вашого алгоритму в JavaScript для оцінки результатів. Він генерує 100 000 випадкових цілих чисел від 0 - 99 і відстежує примірник кожного цілого числа.

Перше, що я помічаю, це те, що ви швидше отримуєте низьке число, ніж велике. Ви бачите це найбільше, коли seed1високий і seed2низький. За пару примірників я отримав лише 3 номери.

У кращому випадку ваш алгоритм потребує доопрацювання.


8

Якщо Math.Random() функція викликає операційну систему, щоб отримати час доби, ви не можете порівняти її зі своєю функцією. Ваша функція - PRNG, тоді як ця функція прагне до реальних випадкових чисел. Яблука та апельсини.

Ваш PRNG може бути швидким, але у нього не вистачає інформації про стан, щоб досягти тривалого періоду до того, як він повториться (і його логіка недостатньо складна, щоб навіть досягти періодів, можливих за такої кількості інформації про стан).

Період - це тривалість послідовності, перш ніж ваш PRNG почне повторюватися. Це відбувається, як тільки машина PRNG робить перехід стану до стану, ідентичного деякому минулому. Звідти він повторить переходи, які почалися в такому стані. Ще однією проблемою PRNG може бути мала кількість унікальних послідовностей, а також вироджена конвергенція певної послідовності, яка повторюється. Також можуть бути небажані візерунки. Наприклад, припустимо, що PRNG виглядає досить випадковим чином, коли числа друкуються у десятковій формі, але перевірка значень у двійковій формі показує, що біт 4 просто змінюється між 0 і 1 на кожному виклику. На жаль!

Погляньте на Mersenne Twister та інші алгоритми. Існують способи досягти балансу між тривалістю періоду та циклами процесора. Один базовий підхід (застосовується в Mersenne Twister) - кругообіг у векторі стану. Тобто, коли генерується число, воно базується не на всьому стані, а лише на кількох словах із масиву стану, що підлягають декільком бітовим операціям. Але на кожному кроці алгоритм також рухається по масиву, трохи розбираючи вміст за один раз.


5
Я переважно згоден, за винятком вашого першого абзацу. Вбудовані випадкові дзвінки (та / dev / random у Unix-подібних системах) також є PRNG. Я б назвав все, що алгоритмічно створює випадкові числа, PRNG, навіть якщо насіння - це важко передбачити. Існує декілька "справжніх" генераторів випадкових чисел, які використовують радіоактивний розпад, атмосферний шум тощо, але вони часто генерують порівняно мало біт / секунду.
Метт Крауз

На скриньках Linux /dev/random- це джерело реальної випадковості, отримане від драйверів пристроїв, а не PRNG. Блокується, коли доступно недостатньо бітів. Сестринський пристрій /dev/urandomтакож не блокується, але він все ще не є точно PRNG, оскільки він оновлюється випадковими бітами, коли вони доступні.
Каз

Якщо функція Math.Random () викликає операційну систему, щоб отримати час доби - це абсолютно неправда. (у будь-якому з ароматів / версій Java, які я знаю)
bestsss

@bestsss Це з початкового питання: я пам’ятаю, що десь читав, що Math.random використовував System.nanoTime () . Ваші знання, можливо, варто додати там чи у вашій відповіді. Я використовував його умовно з if . :)
Каз

Kaz, обидва nanoTime()+ counter / хеш використовується для насіння за замовчуванням java.util.Randomoracle / OpenJDK. Це тільки для насіння, тоді це стандартний LCG. Насправді генератор ОП бере 2 випадкових числа для насіння, що нормально - тож немає різниці, ніж java.util.Random. System.currentTimeMillis()було насінням за замовчуванням у JDK1.4-
bestsss

7

Генераторів псевдо випадкових чисел багато, багато. Так , наприклад Кнута ranarray , то смерч Мерсенна , або шукати генератори LFSR. Монументальний «Семінумерічний алгоритми» Кнута аналізує область і пропонує деякі лінійні конгруентні генератори (прості в реалізації, швидкі).

Але я б запропонував вам просто дотримуватися java.util.Randomабо Math.random, вони швидко і принаймні гаразд для епізодичного використання (тобто ігор і подібних). Якщо ви просто параноїдальні в розповсюдженні (якась програма Монте-Карло чи генетичний алгоритм), ознайомтеся з їх реалізацією (джерело десь доступне) та виведіть їх із справді випадковим числом або з вашої операційної системи, або з random.org . Якщо це потрібно для якоїсь програми, де безпека є критичною, вам доведеться викопати себе. І як у такому випадку ви не повинні вірити, який кольоровий квадрат із пропущеними шматочками тут носик, я зараз заткнуся.


7

Дуже малоймовірно, що продуктивність генерації випадкових чисел буде проблемою для будь-якого випадку використання, якщо ви не отримали доступ до одного Randomекземпляра з декількох потоків (тому що Randomце так synchronized).

Однак якщо це дійсно так, і вам потрібно швидко безліч випадкових чисел, ваше рішення є надто ненадійним. Іноді це дає хороші результати, іноді дає жахливі результати (виходячи з початкових налаштувань).

Якщо ви хочете ті самі номери, які Randomдає вам клас, лише швидше, ви можете позбутися синхронізації там:

public class QuickRandom {

    private long seed;

    private static final long MULTIPLIER = 0x5DEECE66DL;
    private static final long ADDEND = 0xBL;
    private static final long MASK = (1L << 48) - 1;

    public QuickRandom() {
        this((8682522807148012L * 181783497276652981L) ^ System.nanoTime());
    }

    public QuickRandom(long seed) {
        this.seed = (seed ^ MULTIPLIER) & MASK;
    }

    public double nextDouble() {
        return (((long)(next(26)) << 27) + next(27)) / (double)(1L << 53);
    }

    private int next(int bits) {
        seed = (seed * MULTIPLIER + ADDEND) & MASK;
        return (int)(seed >>> (48 - bits));
    }

}

Я просто взяв java.util.Randomкод і видалив синхронізацію, що призводить до подвійної продуктивності порівняно з оригіналом на моєму Oracle HotSpot JVM 7u9. Він все ще повільніше, ніж ваш QuickRandom, але дає набагато більш послідовні результати. Якщо бути точним, для одних seedі тих же значень та однопотокових програм це дає ті самі псевдовипадкові числа, як і оригінальний Randomклас.


Цей код заснований на поточному java.util.Randomв OpenJDK 7u, який ліцензований відповідно до GNU GPL v2 .


EDIT через 10 місяців:

Я щойно виявив, що вам навіть не потрібно використовувати мій код вище, щоб отримати несинхронізований Randomекземпляр. У JDK теж є одна!

Подивіться на ThreadLocalRandomклас Java 7 . Код всередині нього майже ідентичний моєму коду вище. Клас - це просто локальний потікRandom версія, придатна для швидкого генерування випадкових чисел. Єдиний недолік, про який я можу подумати - це те, що ви не можете встановити його seedвручну.

Приклад використання:

Random random = ThreadLocalRandom.current();

2
@Edit Хм, я можу порівняти QR, Math.random та ThreadLocalRandom колись, коли я не лінивий :)Це цікаво, дякую!
tckmn

1. Ви можете набрати трохи більше швидкості, скинувши маску, оскільки найвищі 16 біт не впливають на використані біти. 2. Ви можете використовувати ці біти, зберегти одне віднімання і отримати кращий генератор (більший стан; найбільш значущі біти продукту є найбільш добре розподіленими, але потрібна деяка оцінка). 3. Хлопці Sun просто запровадили архаїчний RNG від Knuth і додали синхронізацію. :(
maaartinus

3

"Випадкові" - це більше ніж просто отримання чисел .... те, що у вас є, є псевдовипадковим

Якщо псевдовипадковий достатньо хороший для ваших цілей, тоді впевнено, це швидше (і XOR + Bitshift буде швидше, ніж у вас є)

Рольф

Редагувати:

Гаразд, після занадто поспішної у цій відповіді, дозвольте мені відповісти на справжню причину, чому ваш код швидше:

Від JavaDoc для Math.Random ()

Цей спосіб належним чином синхронізований, щоб дозволити правильне використання більш ніж однією ниткою. Однак якщо багатьом потокам потрібно генерувати псевдовипадкові числа з великою швидкістю, це може зменшити суперечку для кожного потоку, щоб він мав власний генератор псевдовипадкових чисел.

Ймовірно, чому ваш код швидший.


3
Практично все, що не включає апаратний генератор шуму або пряму лінію в I / O операційної системи ОС, буде псевдовипадковою. Справжню випадковість не можна генерувати лише алгоритмом; тобі потрібен шум звідкись. (РНГ деяких ОС отримують свій внесок, вимірюючи такі речі, як, як / коли ви переміщуєте мишу, набираєте речі тощо. Вимірюється за шкалою від мікросекунд до наносекунд, що може бути дуже непередбачуваним.)
cHao

@OliCharlesworth: дійсно, наскільки я знаю, єдині справжні випадкові величини виявляються за допомогою атмосферного шуму.
Jeroen Vannevel

@me ... нерозумно відповідати поспішно. Math.random є псевдовипадковою, а також синхронізованою .
rolfl

@rolfl: Синхронізація може дуже добре пояснити, чому Math.random()це повільніше. Було б або потрібно синхронізувати або створювати нове Randomщоразу, і жодне не дуже привабливе виконання. Якби я дбав про продуктивність, я створив би свою власну new Randomі просто скористався цим. : P
cHao

Радіоактивний розпад @ JeroenVannevel теж випадковий.
RxS

3

java.util.Random не сильно відрізняється - базовий LCG, описаний Кнутом. Однак він має 2 основні переваги / відмінності:

  • безпечний потік - кожне оновлення - це CAS, який дорожче, ніж просте записування, і потребує гілки (навіть якщо ідеально передбачена одна нитка). Залежно від процесора це може бути суттєвою різницею.
  • нерозкритий внутрішній стан - це дуже важливо для будь-якого нетривіального. Ви хочете, щоб випадкові числа не були передбачуваними.

Нижче це основна програма, що генерує "випадкові" цілі числа в java.util.Random.


  protected int next(int bits) {
        long oldseed, nextseed;
        AtomicLong seed = this.seed;
        do {
          oldseed = seed.get();
          nextseed = (oldseed * multiplier + addend) & mask;
        } while (!seed.compareAndSet(oldseed, nextseed));
        return (int)(nextseed >>> (48 - bits));
    }

Якщо ви вилучите AtomicLong та нерозкриту кількість сайтів (тобто, використовуючи всі біти long), ви отримаєте більшу продуктивність, ніж подвійне множення / модуль.

Останнє зауваження: Math.randomйого не слід використовувати лише для простих тестів, воно схильне до суперечок, і якщо у вас є навіть пара потоків, що називають його одночасно, продуктивність погіршує. Однією з маловідомих історичних особливостей цього є введення CAS в java - перемогти сумнозвісний орієнтир (спочатку IBM через внутрішню техніку, а потім НД зробив "CAS з Java")


0

Це випадкова функція, яку я використовую для своїх ігор. Він досить швидкий і має хороший (достатній) розподіл.

public class FastRandom {

    public static int randSeed;

      public static final int random()
      {
        // this makes a 'nod' to being potentially called from multiple threads
        int seed = randSeed;

        seed    *= 1103515245;
        seed    += 12345;
        randSeed = seed;
        return seed;
      }

      public static final int random(int range)
      {
        return ((random()>>>15) * range) >>> 17;
      }

      public static final boolean randomBoolean()
      {
         return random() > 0;
      }

       public static final float randomFloat()
       {
         return (random()>>>8) * (1.f/(1<<24));
       }

       public static final double randomDouble() {
           return (random()>>>8) * (1.0/(1<<24));
       }
}

1
Це не дає відповіді на запитання. Щоб критикувати або вимагати роз'яснення у автора, залиште коментар під їх публікацією.
Джон Віллемс

Я думаю, вже було встановлено, що оригінальний алгоритм недостатньо хороший? Можливо, приклад того, що достатньо добре, може призвести до натхнення, як його вдосконалити?
Terje

Так, можливо, але це зовсім не відповідає на запитання, і немає даних, що підтримують ваш алгоритм, насправді "досить хороший". Як правило, алгоритми випадкових чисел та тісно пов’язані з ними алгоритми шифрування ніколи не такі хороші, як ті, які використовували експерти, які реалізували їх мовою програмування. Отже, якби ви могли підтримати свою заяву та детально пояснити, чому це краще, ніж алгоритм у питанні, ви хоч би відповіли на поставлене запитання.
Джон Віллемс

Ну ... Експерти, які реалізували їх мовою програмування, прагнуть до "ідеального" розповсюдження, тоді як в грі вам це ніколи не потрібно. Ви хочете швидкості та "досить хорошого" розподілу. Цей код пропонує це. Якщо тут це недоречно, відповідь я видалю, жодних проблем.
Terje

Що стосується багатопотокової редакції, то використання локальної змінної є необов'язковим, тому що без volatileцього компілятор вільний для власного усунення (або введення) локальних змінних.
maaartinus
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.