Посев 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, и я бы хотел избежать эта ситуация в будущем.

Ответ 1

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

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

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

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

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

Ответ находится в исходном коде для java.util.Random, который является линейным конгруэнтным RNG. Когда вы указываете семя в конструкторе, его обрабатывают следующим образом.

seed = (seed ^ 0x5DEECE66DL) & mask;

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

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

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

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

101110110010000010110100011000000000101001110100
101110110001101011010101011100110010010000000111
101110110010110001110010001110011101011101001110
101110110010011010010011010011001111000011100001

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

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

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

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

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

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

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

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

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