Является ли это «достаточно хорошим» случайным алгоритмом; почему не используется, если это быстрее?

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так много, когда кажется, что достаточно простого умножения и вырезания десятичной дроби?
tckmn
источник
154
«кажется довольно случайным»; Вы должны сгенерировать гистограмму и выполнить некоторую автокорреляцию в своей последовательности ...
Оливер Чарлсворт,
63
Он означает, что «кажется довольно случайным» на самом деле не объективная мера случайности, и вы должны получить некоторую фактическую статистику.
Мэтт Х
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  :                                                  
BalusC
источник
17
+1 для числовых данных - хотя поиск необработанных чисел может вводить в заблуждение, поскольку это не означает, что они имеют статистически значимое различие.
Мацей Печотка
16
Эти результаты сильно различаются в зависимости от начальных семян QuickRandom. Иногда это близко к форме, иногда намного хуже, чем это.
Петр Янечек
68
@ BlueRaja-DannyPflughoeft Мне кажется, что любой PRNG, в котором качество выходных данных сильно зависит от начальных значений (в отличие от внутренних констант), мне не подходит.
CVN
22
Первое правило статистики: выведите данные . Ваш анализ точен, но построение гистограммы показывает это намного быстрее. ;-) (И это две строки в R.)
Конрад Рудольф
37
Обязательные цитаты: «Любой, кто рассматривает арифметические методы получения случайных цифр, конечно, находится в состоянии греха». - Джон фон Нейман (1951). «Тот, кто не видел приведенную цитату хотя бы в 100 местах, вероятно, не очень стар». - Д.В. Прайор (1993) «Генераторы случайных чисел не должны выбираться случайным образом». - Дональд Кнут (1986)
Happy Green Kid Naps
133

То, что вы описываете, является типом случайного генератора, называемого линейным конгруэнтным генератором . Генератор работает следующим образом:

  • Начните с начального значения и множителя.
  • Чтобы сгенерировать случайное число:
    • Умножьте семя на множитель.
    • Установите семя, равное этому значению.
    • Верните это значение.

Этот генератор имеет много хороших свойств, но имеет значительные проблемы как хороший случайный источник. В статье Википедии, ссылки на которую приведены выше, описаны некоторые сильные и слабые стороны. Короче говоря, если вам нужны хорошие случайные значения, это, вероятно, не очень хороший подход.

Надеюсь это поможет!

templatetypedef
источник
@ louism- По сути, это не случайно. Результаты будут детерминированными. Тем не менее, я не думал об этом, когда писал свой ответ; возможно, кто-то сможет уточнить эту деталь?
templatetypedef
2
Арифметические ошибки с плавающей точкой являются разработкой реализации. Насколько я знаю, они совместимы для определенной платформы, но могут отличаться, например, между различными мобильными телефонами и между архитектурами ПК. Хотя при выполнении серии вычислений с плавающей запятой в ряд иногда добавляются дополнительные «защитные биты», а наличие или отсутствие этих защитных битов может незначительно отличать вычисления в результате. (защитные биты, например, расширение от 64-битного двойного до 80-битного)
Паташу
2
Также имейте в виду, что теория LCRNG предполагает, что вы работаете с целыми числами! Бросая числа с плавающей точкой в ​​это не даст того же качества результатов.
duskwuff -неактивный-
1
@duskwuff, ты прав. Но если аппаратные средства с плавающей запятой действительно следуют нормальным правилам, то делать это так же, как делать это по модулю размера мантиссы, и теория применима. Просто нужна дополнительная осторожность в том, что ты делаешь.
vonbrand
113

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

0.10 -> 0.20

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

0.09 -> 0.18
0.11 -> 0.22

Во многих случаях это создаст заметные корреляции в вашей игре - например, если вы выполняете последовательные вызовы своей функции для генерации координат X и Y для объектов, объекты будут образовывать четкие диагональные структуры.

Если у вас нет веских оснований полагать, что генератор случайных чисел замедляет работу вашего приложения (а это ОЧЕНЬ маловероятно), нет веских оснований пытаться написать свое собственное.

сумеречный
источник
36
+1 для практического ответа ... использовать это в перестрелке и вызывать врагов вдоль диагоналей для эпических множественных выстрелов в голову? : D
WIM
@ 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 намного более эстетично, потому что оно текстурировано как ковер из морских водорослей.
Крис Стрингфеллоу
Эстетически приятная часть - то, как последовательность имеет тенденцию увеличиваться, когда вы идете вдоль каждой строки (и затем снова возвращаетесь к началу), поскольку magicNumberумножение производит число, похожее на prevNum, что показывает отсутствие случайности. Если мы используем семена, new QuickRandom(0.01, 1.03)то мы получим это i.imgur.com/Q1Yunbe.png !
Каллум Роджерс
Да, отличный анализ. Так как он просто умножает модуль 1 на константу до того, как произойдет перенос, произойдет увеличение, которое вы описываете. Похоже, этого можно было бы избежать, если бы мы взяли менее значимые десятичные разряды, скажем, умножив на 1 млрд, а затем уменьшив мод до 256 цветовой палитры.
Крис Стрингфеллоу
Можете ли вы сказать мне, что вы использовали для генерации этих выходных изображений? Matlab?
день
@uDaY: взгляните на код, C # и System.Drawing.Bitmap.
Каллум Роджерс
37

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

Еще одна вещь, которую нужно учитывать, это период вашего генератора случайных чисел. Очевидно, что с конечным размером состояния, равным порции двойного мантиссы, он сможет возвращать самое большее 2 ^ 52 значений перед циклом. Но это в лучшем случае - можете ли вы доказать, что нет циклов периода 1, 2, 3, 4 ...? Если это так, ваш ГСЧ будет иметь ужасное, вырожденное поведение в этих случаях.

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

Если вы можете ответить на все эти вопросы, круто. Если вы не можете, то вы знаете, почему большинство людей не изобретают велосипед и не используют проверенный генератор случайных чисел;)

(Кстати, хорошая пословица такова: самый быстрый код - это код, который не запускается. Вы можете сделать самый быстрый случайный () в мире, но это не хорошо, если он не очень случайный)

Patashu
источник
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. Преобразовать вывод в значения char
  2. Записать значение символа в файл
  3. Сжать файл

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

Если бы я был действительно придирчив, я мог бы взять хорошие алгоритмы и запустить на них тесты DIEHARD / NIST, чтобы получить больше понимания, а затем вернуться и немного доработать.

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

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

Поскольку большинство алгоритмов сжатия являются быстрыми и не требуют реализации (так как в ОС они просто лежат), тест на сжатие очень полезен для быстрой оценки прохождения / отказа для ГСЧ, которую вы разрабатываете.

Удачи в ваших экспериментах!

О, я выполнил этот тест на вашей программе, используя следующий небольшой мод вашего кода:

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
Спасибо @ Александр и Аристос и помощник. Я верю тебе.
Крис Стрингфеллоу
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] Седжевик Роберт, Алгоритмы на С , Addinson Wesley Publishing Company, 1990, страницы с 516 по 518

higuaro
источник
9
+1 для xkcd, который является удивительным рабочим сайтом (о, и отличный ответ): P
tckmn
1
Спасибо, да и стойки xkcd! XD
Higuaro
Теория хорошая, но исполнение плохое: код подвержен целочисленному переполнению. В Java все int[]инициализируются на ноль, поэтому нет необходимости в этой части. Бросать на плаву бессмысленно, когда вы работаете с двойниками. Последнее: вызывать имена методов random1 и random2 довольно забавно.
bestsss
@bestsss Спасибо за наблюдения! Я сделал прямой перевод из кода C и не обращал на него особого внимания = (. Я внес некоторые изменения и обновил ответ. Буду признателен за любые дополнительные предложения
higuaro
14

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

Первое, что я замечаю, это то, что у вас больше шансов получить меньшее число, чем большое. Вы видите это больше всего, когда seed1высоко иseed2 низко. В нескольких случаях я получил только 3 номера.

В лучшем случае ваш алгоритм нуждается в доработке.

gilly3
источник
8

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

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

Период - это длина последовательности до того, как ваш PRNG начнет повторяться. Это происходит, как только машина PRNG переходит в состояние, идентичное некоторому прошлому состоянию. Оттуда он будет повторять переходы, которые начались в этом состоянии. Другая проблема с PRNG может заключаться в небольшом количестве уникальных последовательностей, а также в вырожденной сходимости на определенной последовательности, которая повторяется. Также могут быть нежелательные шаблоны. Например, предположим, что PRNG выглядит довольно случайным, когда числа печатаются в десятичном виде, но проверка значений в двоичном формате показывает, что бит 4 просто переключается между 0 и 1 при каждом вызове. К сожалению!

Взгляните на Mersenne Twister и другие алгоритмы. Есть способы найти баланс между продолжительностью периода и циклами ЦП. Один из основных подходов (используется в Twister Mersenne) заключается в циклическом переключении вектора состояния. То есть, когда число генерируется, оно не основано на полном состоянии, только на нескольких словах из массива состояний, подвергаемых нескольким битовым операциям. Но на каждом этапе алгоритм также перемещается в массиве, разбирая содержимое по частям.

Kaz
источник
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 / hash используются по умолчанию для семени java.util.Randomoracle / OpenJDK. Это только для семян, тогда это стандартный LCG. По сути, генератор OP берет 2 случайных числа для начального числа, что нормально - так что никакой разницы, чем java.util.Random. System.currentTimeMillis()был семенем по умолчанию в JDK1.4-
bestsss
7

Существует множество генераторов псевдослучайных чисел. Например, ранаррай Кнута , твистер Мерсенна или поиск генераторов LFSR. Монументальные «Получисленные алгоритмы» Кнута анализируют область и предлагают некоторые линейные конгруэнтные генераторы (простые в реализации, быстрые).

Но я бы посоветовал вам просто придерживаться java.util.Random или Math.random, они быстро и по крайней мере хорошо для случайного использования (например, игры и тому подобное). Если вы просто параноик по дистрибутиву (какая-то программа Монте-Карло или генетический алгоритм), проверьте их реализацию (где-то источник доступен) и начните с какого-то действительно случайного числа, либо из вашей операционной системы, либо из random.org. , Если это требуется для какого-либо приложения, где безопасность имеет решающее значение, вам придется копать самостоятельно. И поскольку в этом случае вы не должны верить тому, что здесь изображен какой-то цветной квадрат с недостающими битами, я сейчас заткнусь.

vonbrand
источник
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 .


РЕДАКТИРОВАТЬ 10 месяцев спустя:

Я только что обнаружил, что вам даже не нужно использовать мой код выше, чтобы получить несинхронизированный Randomэкземпляр. В JDK тоже есть один!

Посмотрите на ThreadLocalRandomкласс Java 7 . Код внутри него почти идентичен моему коду выше. Класс представляет собой просто изолированную от локального потока Randomверсию, подходящую для быстрой генерации случайных чисел. Единственный недостаток, о котором я могу думать, это то, что вы не можете установить его seedвручную.

Пример использования:

Random random = ThreadLocalRandom.current();
Петр Янечек
источник
2
@ Edit Хм, я могу сравнить QR, Math.random и ThreadLocalRandom, когда мне не лень. :)Это интересно, спасибо!
13
1. Вы можете увеличить скорость, сбросив маску, поскольку старшие 16 бит не влияют на используемые биты. 2. Вы можете использовать эти биты, сохранить одно вычитание и получить лучший генератор (большее состояние; наиболее значимые биты продукта распределены наиболее хорошо, но потребуется некоторая оценка). 3. Парни из Sun просто внедрили архаичный ГСЧ от Кнута и добавили синхронизацию. :(
maaartinus
3

«Случайный» - это больше, чем просто получение чисел… псевдослучайным

Если псевдослучайность достаточно хороша для ваших целей, то, конечно, она намного быстрее (и XOR + Bitshift будет быстрее, чем у вас)

Рольф

Редактировать:

Хорошо, поспешно ответив на этот вопрос, позвольте мне ответить на реальную причину, по которой ваш код работает быстрее:

Из JavaDoc для Math.Random ()

Этот метод правильно синхронизирован, чтобы обеспечить правильное использование более чем одним потоком. Однако, если многим потокам нужно генерировать псевдослучайные числа с большой скоростью, это может уменьшить конкуренцию за то, что у каждого потока будет свой собственный генератор псевдослучайных чисел.

Вероятно, поэтому ваш код работает быстрее.

rolfl
источник
3
Практически все, что не связано с аппаратным генератором шума или прямой связью с операциями ввода / вывода ОС, будет псевдослучайным. Подлинная случайность не может быть сгенерирована одним алгоритмом; вам нужен шум откуда-то. (ГСЧ некоторых ОС получают свои данные, измеряя такие вещи, как, как / когда вы перемещаете мышь, печатаете вещи и т. Д. Измеряется по шкале от микросекунд до наносекунд, что может быть крайне непредсказуемым.)
cHao
@OliCharlesworth: действительно, насколько я знаю, единственные истинные случайные значения находятся с использованием атмосферного шума.
Йероен Ванневел
@ мне ... глупо отвечать на скорую руку. Math.random является псевдослучайным, а также синхронизированным .
rolfl
@rolfl: Синхронизация может очень хорошо объяснить, почему Math.random()медленнее. Он должен был бы либо синхронизироваться, либо создавать новый Randomкаждый раз, и ни один из них не был бы очень привлекательным с точки зрения производительности. Если бы я заботился о производительности, я бы создал свою собственную new Randomи просто использовал ее. : P
cHao
@JeroenVannevel радиоактивный распад тоже случайный.
RxS
3

Случайность не сильно отличается, базовый LCG описан Кнутом. Однако у него есть два основных преимущества / отличия:

  • Потокобезопасный - каждое обновление представляет собой 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 через встроенные функции, а затем Sun сделала «CAS from Java»)

bestsss
источник
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
Это не дает ответа на вопрос. Чтобы критиковать или запросить разъяснения у автора, оставьте комментарий под своим постом.
Джон Виллемс
Я думаю, что уже было установлено, что оригинальный алгоритм не достаточно хорош? Возможно, пример того, что является достаточно хорошим, может вдохновить на то, как его улучшить?
Терье
Да, возможно, но это не отвечает на вопрос вообще, и нет никаких данных, подтверждающих, что ваш алгоритм на самом деле "достаточно хорош". Как правило, алгоритмы случайных чисел и тесно связанные алгоритмы шифрования никогда не были так хороши, как те, которые были реализованы экспертами, которые реализовали их на языке программирования. Итак, если бы вы могли поддержать вашу заявку и уточнить, почему она лучше, чем алгоритм в вопросе, вы бы хотя бы ответили на заданный вопрос.
Джон Виллемс
Хорошо ... Эксперты, которые реализовали их на языке программирования, стремятся к «идеальному» распространению, тогда как в игре вам это никогда не понадобится. Вы хотите скорость и «достаточно хорошее» распределение. Этот код предлагает это. Если это неуместно, я удалю ответ, нет проблем.
Терье
Что касается многопоточности, то использование локальной переменной не допускается, так как без volatileнее компилятор может по своему усмотрению исключать (или вводить) локальные переменные.
Маартин