Заполнение java.util.Random последовательными номерами

Я упростил ошибку, с которой столкнулся, до следующих строк кода:

    int[] vals = new int[8];
    for (int i = 0; i < 1500; i++)
        vals[new Random(i).nextInt(8)]++;
    System.out.println(Arrays.toString(vals));

Вывод: [0, 0, 0, 0, 0, 1310, 190, 0]

Является ли это просто артефактом выбора последовательных чисел для заполнения Random, а затем использования nextInt со степенью 2? Если да, то есть ли другие подводные камни, о которых я должен знать, и если нет, то что я делаю неправильно? (Я не ищу решения вышеуказанной проблемы, просто понимаю, что еще может пойти не так)


Дэн, хорошо написанный анализ. Поскольку в javadoc довольно подробно описано, как рассчитываются числа, это не является загадкой, почему это произошло, поскольку существуют другие подобные аномалии, на которые следует обратить внимание — я не видел никакой документации о последовательных начальных значениях, и я Я надеюсь, что кто-то, имеющий некоторый опыт работы с java.util.Random, может указать на другие распространенные ловушки.

Что касается кода, необходимо, чтобы несколько параллельных агентов имели повторяющееся случайное поведение, которым случается выбирать из перечисления 8 элементов, длина которых является их первым шагом. Как только я обнаружил это поведение, все семена исходят из главного случайного объекта, созданного из известного семени. В прежней (последовательной) версии программы все поведение быстро расходилось после этого первого вызова nextInt, поэтому мне потребовалось довольно много времени, чтобы сузить поведение программы до библиотеки RNG, и я хотел бы избежать эту ситуацию в будущем.


person Parker    schedule 08.01.2009    source источник
comment
Какой цели служит повторное заполнение генератора?   -  person jjnguy    schedule 09.01.2009
comment
согласен с jjnguy. Почему вы не выполняете Random r = new Random(x) вне цикла, а просто вызываете r.nextInt() снова и снова? похоже, вы делаете гистограмму того, как часто появляются числа 0-7, и вы ожидаете, что она будет относительно одинаковой...   -  person John Gardner    schedule 09.01.2009
comment
Просто используйте r = new Random(), который, как я полагаю, запустит его с текущего времени.   -  person Steve Kuo    schedule 10.01.2009


Ответы (1)


Насколько это возможно, начальное число для ГСЧ само по себе должно быть случайным. Семена, которые вы используете, будут отличаться только одним или двумя битами.

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

Просто создайте один ГСЧ и используйте его повторно, тогда у вас не будет этой проблемы.

В ответ на комментарий mmyers:

Вы случайно не знаете java.util.Random достаточно хорошо, чтобы объяснить, почему в данном случае он выбирает 5 и 6?

Ответ содержится в исходном коде java.util.Random, который представляет собой линейный конгруэнтный ГСЧ. . Когда вы указываете начальное значение в конструкторе, оно обрабатывается следующим образом.

seed = (seed ^ 0x5DEECE66DL) & mask;

Где маска просто сохраняет младшие 48 бит и отбрасывает остальные.

При генерации фактических случайных битов это начальное число обрабатывается следующим образом:

randomBits = (seed * 0x5DEECE66DL + 0xBL) & mask;

Теперь, если вы считаете, что семена, используемые Parker, были последовательными (0-1499), и они использовались один раз, а затем отбрасывались, первые четыре начальных числа генерировали следующие четыре набора случайных битов:

101110110010000010110100011000000000101001110100
101110110001101011010101011100110010010000000111
101110110010110001110010001110011101011101001110
101110110010011010010011010011001111000011100001

Обратите внимание, что верхние 10 битов в каждом случае идентичны. Это проблема, потому что он хочет генерировать значения только в диапазоне 0-7 (для чего требуется всего несколько битов), а реализация ГСЧ делает это, сдвигая старшие биты вправо и отбрасывая младшие биты. Это происходит потому, что в общем случае старшие биты более случайны, чем младшие. В данном случае это не так, потому что исходные данные были плохими.

Наконец, чтобы увидеть, как эти биты преобразуются в десятичные значения, которые мы получаем, вам нужно знать, что java.util.Random создает особый случай, когда n является степенью двойки. Он запрашивает 31 случайный бит (31 верхний бит из выше 48), умножает это значение на n и затем сдвигает его на 31 бит вправо.

Умножение на 8 (значение n в этом примере) равносильно сдвигу влево на 3 разряда. Таким образом, чистый эффект этой процедуры заключается в сдвиге 31 бита на 28 позиций вправо. В каждом из 4 приведенных выше примеров остается битовый шаблон 101 (или 5 в десятичном формате).

Если бы мы не отбрасывали ГСЧ только после одного значения, мы бы увидели, что последовательности расходятся. В то время как четыре последовательности выше всех начинаются с 5, вторые значения каждой из них равны 6, 0, 2 и 4 соответственно. Небольшие различия в исходных семенах начинают оказывать влияние.

В ответ на обновленный вопрос: java.util.Random является потокобезопасным, вы можете совместно использовать один экземпляр в нескольких потоках, поэтому нет необходимости иметь несколько экземпляров. Если вам действительно нужно иметь несколько экземпляров ГСЧ, убедитесь, что они заполняются полностью независимо друг от друга, иначе вы не можете быть уверены, что выходные данные будут независимыми.

Что касается того, почему вы получаете такие эффекты, java.util.Random не лучший ГСЧ. Это просто, довольно быстро и, если не присматриваться, достаточно случайно. Однако, если вы проведете несколько серьезных тестов его выходных данных, вы увидите, что он ошибочен. Вы можете увидеть это визуально здесь.

Если вам нужен более случайный RNG, вы можете использовать java.security.SecureRandom. Это немного медленнее, но работает правильно. Одна вещь, которая может быть проблемой для вас, заключается в том, что это невозможно повторить. Два экземпляра SecureRandom с одним и тем же начальным числом не дадут одинаковых результатов. Это по дизайну.

Итак, какие еще есть варианты? Здесь я подключаю мою собственную библиотеку. Он включает в себя 3 повторяющихся псевдо-ГСЧ, которые быстрее, чем SecureRandom, и более случайны, чем java.util.Random. Я их не изобретал, я просто перенес их из оригинальных версий C. Все они потокобезопасны.

Я реализовал эти ГСЧ, потому что мне нужно было что-то лучшее для моего эволюционного вычислительного кода. В соответствии с моим первоначальным кратким ответом этот код является многопоточным, но использует только один экземпляр ГСЧ, общий для всех потоков.

person Dan Dyer    schedule 08.01.2009
comment
Совершенно верно, но теперь он меня все любопытно. Я знаю, что вы работали с ГСЧ; Вы случайно не знаете java.util.Random достаточно хорошо, чтобы объяснить, почему в данном случае он выбирает 5 и 6? - person Michael Myers; 09.01.2009