Что с 181783497276652981 и 8682522807148012 в случайном порядке (Java 7)?

112

Почему были 181783497276652981и 8682522807148012выбраны Random.java?

Вот соответствующий исходный код из Java SE JDK 1.7:

/**
 * Creates a new random number generator. This constructor sets
 * the seed of the random number generator to a value very likely
 * to be distinct from any other invocation of this constructor.
 */
public Random() {
    this(seedUniquifier() ^ System.nanoTime());
}

private static long seedUniquifier() {
    // L'Ecuyer, "Tables of Linear Congruential Generators of
    // Different Sizes and Good Lattice Structure", 1999
    for (;;) {
        long current = seedUniquifier.get();
        long next = current * 181783497276652981L;
        if (seedUniquifier.compareAndSet(current, next))
            return next;
    }
}

private static final AtomicLong seedUniquifier
    = new AtomicLong(8682522807148012L);

Таким образом, при вызове new Random()без какого-либо начального параметра берется текущий «начальный унификатор» и выполняется XOR с ним System.nanoTime(). Затем он использует 181783497276652981для создания другого уникального начального значения, которое будет сохранено для следующего вызова new Random().

Литералы 181783497276652981Lи 8682522807148012Lне помещаются в константы, но больше нигде не встречаются.

Сначала комментарий дает мне легкую зацепку. Поиск этой статьи в Интернете дает саму статью . 8682522807148012не появляется в работе, но 181783497276652981действительно появляется - как подстроки другого числа, 1181783497276652981, что 181783497276652981с 1префиксом.

В статье утверждается, что 1181783497276652981это число дает хорошие «достоинства» линейного конгруэнтного генератора. Этот номер просто неправильно скопировали в Java? Есть ли 181783497276652981приемлемые достоинства?

А почему был 8682522807148012выбран?

Поиск в Интернете любого числа не дает никаких объяснений, только эта страница, которая также замечает выпавшее 1впереди 181783497276652981.

Могли ли быть выбраны другие числа, которые работали бы так же хорошо, как эти два числа? Почему или почему нет?

rgettman
источник
Я просто хотел бы отметить, что ни одна из упомянутых констант (даже самые большие с теми, что указаны в начале) не слишком велики, чтобы уместиться, хотя умножение обязательно приведет к переполнению.
нанофарад
6
8682522807148012является наследием предыдущей версии класса, как видно из изменений, внесенных в 2010 году . 181783497276652981L, Кажется, опечатка , действительно , и вы можете подать отчет об ошибке.
assylias
6
Либо это опечатка, то есть ошибка, либо функция с нераскрытой мотивацией. Вы должны спросить авторов. Все, что вы здесь получите, будет просто более или менее неосведомленным мнением. Если вы считаете, что это ошибка, отправьте отчет об ошибке.
Маркиз Лорн,
1
Учитывая разные ответы, это может быть два отдельных вопроса для каждой константы.
Марк Херд
1
Печально видеть глобальное узкое место масштабируемости, встроенное в такой фундаментальный класс. seedUniquifierна 64-ядерном ящике может быть очень много. Локальный поток был бы более масштабируемым.
usr

Ответы:

57
  1. Этот номер просто неправильно скопировали в Java?

    Да вроде опечатка.

  2. Имеет ли 181783497276652981 приемлемую заслугу?

    Это можно определить с помощью алгоритма оценки, представленного в документе. Но заслуга «оригинального» номера, наверное, выше.

  3. И почему был выбран 8682522807148012?

    Кажется случайным. Это могло быть результатом System.nanoTime () при написании кода.

  4. Могли ли быть выбраны другие числа, которые работали бы так же хорошо, как эти два числа?

    Не все числа будут одинаково "хорошими". Так что нет.

Стратегии посева

Существуют различия в схеме заполнения по умолчанию между разными версиями и реализацией JRE.

public Random() { this(System.currentTimeMillis()); }
public Random() { this(++seedUniquifier + System.nanoTime()); }
public Random() { this(seedUniquifier() ^ System.nanoTime()); }

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

Второй не является потокобезопасным. Несколько потоков могут получить одинаковые RNG при инициализации одновременно. Кроме того, начальные значения последующих инициализаций обычно коррелируют. В зависимости от фактического разрешения таймера системы, начальная последовательность может линейно увеличиваться (n, n + 1, n + 2, ...). Как указано в разделе «Насколько разными должны быть случайные семена?» и упомянутый документ Общие дефекты при инициализации генераторов псевдослучайных чисел , коррелированные начальные числа могут генерировать корреляцию между фактическими последовательностями множества ГСЧ.

Третий подход создает случайно распределенные и, следовательно, некоррелированные начальные числа, даже между потоками и последующими инициализациями. Итак, текущие java-документы:

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

может быть расширен «по потокам» и «некоррелирован»

Качество посевной последовательности

Но случайность последовательности раздачи настолько хороша, насколько хорош основной ГСЧ. ГСЧ, используемый для начальной последовательности в этой реализации Java, использует мультипликативный линейный конгруэнтный генератор (MLCG) с c = 0 и m = 2 ^ 64. (Модуль 2 ^ 64 неявно задается переполнением 64-битных длинных целых чисел) Из-за нулевого значения c и модуля степени 2 «качество» (длина цикла, битовая корреляция, ...) ограничено . Как говорится в документе, помимо общей длины цикла, каждый бит имеет собственную длину цикла, которая экспоненциально уменьшается для менее значимых битов. Таким образом, младшие биты имеют меньший шаблон повторения. (Результат seedUniquifier () должен быть перевернут, прежде чем он будет усечен до 48 бит в фактическом ГСЧ)

Но это быстро! И чтобы избежать ненужных циклов сравнения и установки, тело цикла должно быть быстрым. Это, вероятно, объясняет использование этого конкретного MLCG без сложения, без xoring, только с одним умножением.

И в упомянутой статье представлен список хороших «множителей» для c = 0 и m = 2 ^ 64, как 1181783497276652981.

В общем: А за старания @ JRE-developers;) Но есть опечатка. (Но кто знает, если кто-то не оценит это, есть вероятность, что отсутствующая ведущая 1 действительно улучшает начальный ГСЧ.)

Но некоторые множители определенно хуже: «1» ведет к постоянной последовательности. «2» приводит к однобитовой последовательности (как-то коррелированной) ...

Корреляция между последовательностями для ГСЧ действительно актуальна для моделирования (Монте-Карло), когда несколько случайных последовательностей создаются и даже распараллеливаются. Таким образом, для получения «независимых» прогонов моделирования необходима хорошая стратегия посева. Поэтому стандарт C ++ 11 вводит концепцию Seed Sequence для генерации некоррелированных начальных чисел.

Томас Б.
источник
3
По крайней мере, это все еще странно, если бы они отбросили наименее значимое вместо наиболее значимого, тогда каждое умножение немного теряет, пока в конечном итоге (после 62 шагов) он seedUniquifierне застрянет на нуле.
Гарольд
9

Если учесть, что уравнение, используемое для генератора случайных чисел, выглядит следующим образом:

LCGEquation

Где X (n + 1) - следующее число, a - множитель, X (n) - текущее число, c - приращение, а m - модуль.

Если вы посмотрите дальше Random, a, c и m определены в заголовке класса

private static final long multiplier = 0x5DEECE66DL;   //= 25214903917 -- 'a'
private static final long addend = 0xBL;               //= 11          -- 'c'
private static final long mask = (1L << 48) - 1;       //= 2 ^ 48 - 1  -- 'm'

и глядя на метод protected int next(int bits), в котором реализовано уравнение

nextseed = (oldseed * multiplier + addend) & mask;
//X(n+1) =  (X(n)   *      a     +    c  ) mod m

Это означает, что метод seedUniquifier()фактически получает X (n) или, в первом случае, при инициализации X (0), что на самом деле 8682522807148012 * 181783497276652981, это значение затем дополнительно изменяется на значение System.nanoTime(). Этот алгоритм согласуется с приведенным выше уравнением, но со следующими X (0) = 8682522807148012, a = 181783497276652981, m = 2 ^ 64 и c = 0. Но поскольку модуль m of формируется длинным переполнением, приведенное выше уравнение просто становится

eq2

Глядя на бумагу , значение a = 1181783497276652981для m = 2 ^ 64, c = 0. Таким образом, похоже, что это просто опечатка, а значение 8682522807148012для X (0), которое кажется случайно выбранным числом из устаревшего кода. для Random. Как видно здесь. Но достоинства этих выбранных чисел все еще могут быть в силе, но, как было сказано Томасом Б., вероятно, не столь "хороши", как в статье.

РЕДАКТИРОВАТЬ - Ниже исходные мысли были прояснены, поэтому их можно игнорировать, но оставив для справки.

Это подводит меня к выводам:

  1. Ссылка на документ касается не самого значения, а методов, используемых для получения значений из-за различных значений a, c и m.

  2. Это простое совпадение, что значение в остальном то же самое, кроме ведущей единицы, и комментарий неуместен (хотя все еще изо всех сил пытается в это поверить)

ИЛИ

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

Итак, чтобы ответить на ваш вопрос

Могли ли быть выбраны другие числа, которые работали бы так же хорошо, как эти два числа? Почему или почему нет?

Да, можно было использовать любое число, на самом деле, если вы указываете начальное значение при создании экземпляра Random, вы используете любое другое значение. Это значение не влияет на производительность генератора, это определяется значениями a, c и m, которые жестко запрограммированы в классе.

Ява Дьявол
источник
1
Не совсем - есть два алгоритма: (i) 1 для создания нового случайного начального числа каждый раз, когда вызывается конструктор. Этот алгоритм использует простой X_n + 1 = X_n * a. Из-за длительного переполнения это эквивалентно X_n + 1 = X_n * a mod m. При a = 181783497276652981 и m = 2 ^ 64. (ii) Другой алгоритм, который, начиная с заданного начального числа, производит серию случайных чисел. Вы упомянули второй алгоритм, и в документации поясняется, что « Это генератор линейных конгруэнтных псевдослучайных чисел, как описано Кнутом в книге« Искусство компьютерного программирования ».
assylias
1
@assylias Я понимаю вашу точку зрения, настолько увлекся исходным кодом Randomи цитируемой статьей, что я полностью пропустил исходный вопрос, скоро отредактирую, спасибо.
Java Devil
3

Согласно предоставленной вами ссылке, они выбрали ( после добавления недостающего 1 :) ) лучший выход из 2 ^ 64, потому что долго не может иметь число от 2 ^ 128

Джаффар Рамай
источник