Проблема использования Seed для получения повторяющихся случайных чисел в Go

Go
Проблема использования Seed для получения повторяющихся случайных чисел в Go

1. Повторяющиеся случайные числа

Без лишних слов, давайте сначала рассмотрим очень волшебное явление использования семян.

func main() {
	for i := 0; i < 5; i++ {
    rand.Seed(time.Now().Unix())
		fmt.Println(rand.Intn(100))
	}
}

// 结果如下
// 90
// 90
// 90
// 90
// 90

Возможно, люди, не знакомые с использованием семян, будут очень сбиты с толку, увидев это здесь Разве я не использовал семена все время? Почему все мои случайные числа одинаковы? Разве не должно быть каждый раз по-разному?

Некоторые люди могут сказать, что пространство выборки ваших данных слишком мало. Хорошо, давайте увеличим пространство выборки до 10 Вт и попробуем еще раз.

func main() {
	for i := 0; i < 5; i++ {
    rand.Seed(time.Now().Unix())
		fmt.Println(rand.Intn(100000))
	}
}

// 结果如下
// 84077
// 84077
// 84077
// 84077
// 84077

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

2. Использование семян

Я не буду насрать здесь, давайте сначала сделаем вывод.

Причина того, что одно и то же случайное число получается каждый раз выше, заключается в том, что в приведенном выше цикле интервал каждой операции находится на уровне миллисекунд, поэтому каждый проходtime.Now().Unix()Все извлеченные метки времени имеют одинаковое значение, другими словами, используется одно и то же начальное число.

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

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

Семя используется только для определения детерминированной случайной последовательности. Независимо от того, насколько велико или мало начальное число, до тех пор, пока определена случайная последовательность, она не будет повторяться. Если пространство выборки слишком мало. Есть два решения:

  • Вы можете вызвать seed один раз при глобальной инициализации
  • Используйте наносекундные семена каждый раз (это настоятельно не рекомендуется)

3. Не нужно звонить каждый раз

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

Но что, если он находится в условиях высокой параллелизма? Можете ли вы гарантировать, что он будет отличаться каждый раз, когда вы его получите? Получается, что при высоком параллелизме, даже если в качестве решения использовать UnixNano, будет получена одна и та же временная метка, а официальные лица Go не рекомендуют звонить в одно и то же время в сервис.

Seed should not be called concurrently with any other Rand method.

Далее мы познакомим вас с деталями кода. Если вы хотите узнать исходный код, вы можете читать дальше.

4. Анализ исходного кода-seed

4.1 seed

Давайте сначала посмотрим, что делает семя.

func (rng *rngSource) Seed(seed int64) {
	rng.tap = 0
	rng.feed = rngLen - rngTap

	seed = seed % int32max
	if seed < 0 {  // 如果是负数,则强行转换为一个int32的整数
		seed += int32max
	}
	if seed == 0 { // 如果seed没有被赋值,则默认给一个值
		seed = 89482311
	}

	x := int32(seed)
	for i := -20; i < rngLen; i++ {
		x = seedrand(x)
		if i >= 0 {
			var u int64
			u = int64(x) << 40
			x = seedrand(x)
			u ^= int64(x) << 20
			x = seedrand(x)
			u ^= int64(x)
			u ^= rngCooked[i]
			rng.vec[i] = u
		}
	}
}

Во-первых, seed присваивает две определенные переменные,rng.tapиrng.feed.rngLenиrngTapдве константы. Давайте взглянем на соответствующие определения констант.

const (
	rngLen   = 607
	rngTap   = 273
	rngMax   = 1 << 63
	rngMask  = rngMax - 1
	int32max = (1 << 31) - 1
)

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

Далее мы смотрим на seedrand.

4.2 seedrand

// seed rng x[n+1] = 48271 * x[n] mod (2**31 - 1)
func seedrand(x int32) int32 {
	const (
		A = 48271
		Q = 44488
		R = 3399
	)

	hi := x / Q 	  // 取除数
	lo := x % Q 	  // 取余数
	x = A*lo - R*hi // 通过公式重新给x赋值
	if x < 0 {
		x += int32max // 如果x是负数,则强行转换为一个int32的正整数
	}
	return x
}

Можно видеть, что до тех пор, пока входящий x одинаков, конечный выходной x должен быть таким же. И, наконец, получить случайную последовательностьrng.vecВсе равно.

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

5. Анализ исходного кода-Intn

Во-первых, приведите пример, чтобы интуитивно описать проблему, упомянутую выше.

func printRandom() {
  for i := 0; i < 2; i++ {
    fmt.Println(rand.Intn(100))
  }
}

// 结果
// 81
// 87
// 81
// 87

ПредположениеprintRandom— это один файл Go, поэтому независимо от того, сколько раз вы его запускаете, случайная последовательность, выводимая каждый раз, будет одинаковой. Читая исходный код seed, мы знаем, что это происходит потому, что генерируется одна и та же случайная последовательность. Так почему же он каждый раз получает одно и то же значение? Без дальнейших церемоний, давайте посмотрим на это слой за слоем.

5.1 Intn

func (r *Rand) Intn(n int) int {
	if n <= 0 {
		panic("invalid argument to Intn")
	}
	if n <= 1<<31-1 {
		return int(r.Int31n(int32(n)))
	}
	return int(r.Int63n(int64(n)))
}

Видно, что если n меньше или равно 0, он будет паниковать напрямую. Во-вторых, он вернет соответствующий тип в соответствии с типом входящих данных.

Хотя и сказано, что вызовы здесь делятся на Int31n и Int63n, но, посмотрев вниз, вы обнаружите, что на самом деле они вызываются r.Int63(), но при возврате 64 бит выполняется операция сдвига вправо.

// r.Int31n的调用
func (r *Rand) Int31() int32 { return int32(r.Int63() >> 32) }

// r.Int63n的调用
func (r *Rand) Int63() int64 { return r.src.Int63() }

5.2 Int63

Соответствующий код для этой функции указан первым.

// 返回一个非负的int64伪随机数.
func (rng *rngSource) Int63() int64 {
	return int64(rng.Uint64() & rngMask)
}

func (rng *rngSource) Uint64() uint64 {
	rng.tap--
	if rng.tap < 0 {
		rng.tap += rngLen
	}

	rng.feed--
	if rng.feed < 0 {
		rng.feed += rngLen
	}

	x := rng.vec[rng.feed] + rng.vec[rng.tap]
	rng.vec[rng.feed] = x
	return uint64(x)
}

Видно, что будь то int31 или int63, в конце концов он войдетUint64в этой функции. В этих двух функциях значения этих двух переменных особенно важны. Поскольку окончательное случайное число определяется напрямую, назначения этих двух переменных следующие.

rng.tap = 0
rng.feed = rngLen - rngTap

Значение tap постоянно равно 0, а значение feed определяется rngLen и rngTap, и значение этих двух переменных также является константой. Таким образом, каждый раз значение, взятое из случайной очереди, представляет собой сумму двух определенных значений.

Здесь мы также проверилиПока входящее начальное значение одно и то же, а метод начального числа вызывается каждый раз, случайное значение должно быть одинаковым каждый раз..

6. Заключение

Во-первых, оцените, нужно ли вам использовать начальное число. Во-вторых, вам нужно вызвать начальное значение только один раз глобально. Если вы вызовете его несколько раз, вы можете получить одно и то же случайное число.

Прошлые статьи:

Связанный:

  • Официальная учетная запись WeChat: заметки о полном стеке SH (или прямой поиск WeChat LunhaoHu в интерфейсе добавления официальной учетной записи)