rand()随机函数实现原理
关于“随机数”的产生有许多算法,但无论如何,都不可能产生真正的随机数,因为电脑程序是个确定状态转换机,一种输入必定产生一种确定的输出。
但要实现“不可预知”还是可以做到的,只需有“不可预知”的输入或者初始化就可以了,像系统时间,网络实时带宽,Cpu使用率等等都可以作为“不可预知”的量来初始化随机函数。
产生整数rand的原理是:
y=ax+b(mod n)其中,n一般是一个很大的素数,几万。
a也是大素数。而且a,b,n都是常数。所以rand的产生决定于x,他被称为seed。
每一个seed都是上一次产生的y的函数。这样,如果直接取seed=y的话,
虽然产生的rand之间相关性甚小,但只要知道某个y,就能推知以后的rand。
为避免这种情况,一般取seed为y和当时计算机的时间的函数,如seed=y+t
系统里的随机数是利用初等数论中的同余定理来实现的.
比如VC中对于rand()函数是如下实现的.
int __cdecl rand (void)
{
return(((holdrand =holdrand * 214013L + 2531011L) >> 16)&0x7fff);
}
——————————————————————————————————
- #include <stdlib.h>
- #include <stdio.h>
- int main()
- {
- srand(1);
- for(int i = 0; i < 10; i++)
- {
- printf("%d : %d/n", i, rand());
- }
- }
上面这个例子每次运行得到的结果是一样的, 这是因为对于同样的种子(seed)而言, rand产生的序列是一样的, 所以如果想每次产生的结果不一样的话,需要给srand赋予不同的种子, 比如可以取当前时间作为种子。
2. 原理
实现srand和rand并不困难, 可以采用linearcongruential generator(线性同余)的方法, 用下面的公式表示:
Ij+1 = aIj + c (mod m)
下面的是POSIX.1-2001给出的示范实现:
[cpp] view plaincopy
- static unsigned long next = 1;
- /* RAND_MAX assumed to be 32767 */
- int myrand(void) {
- next = next * 1103515245 + 12345;
- return((unsigned)(next/65536) % 32768);
- }
- void mysrand(unsigned seed) {
- next = seed;
- }
3.VC9和GLIBC2.3的实现
VC9里面CRT代码中可以查到rand和srand的实现, 摘录录下:
[cpp] view plaincopy
- #define RAND_MAX 0x7fff
- void __cdecl srand ( unsigned int seed )
- {
- _getptd()->_holdrand = (unsigned long)seed;
- }
- int __cdecl rand ( void )
- {
- _ptiddata ptd = _getptd();
- return( ((ptd->_holdrand = ptd->_holdrand * 214013L
- + 2531011L) >> 16) & 0x7fff );
- }
可以看出VC里面rand函数返回的最大值是0x7fff,即32767, 由上面的代码可以理解为什么给定同样的seed, 得出的序列是一样的。
Glibc2.3里面关于rand和srand的实现要复杂很多, 采用的non-linearadditive feedback random number generator, rand返回的最大值是2147483647 (2**31 -1), 下面是参考代码:
[cpp] view plaincopy
- weak_alias (__srandom, srand)
- void
- __srandom (x)
- unsigned int x;
- {
- __libc_lock_lock (lock);
- (void) __srandom_r (x, &unsafe_state);
- __libc_lock_unlock (lock);
- }
- int
- __srandom_r (seed, buf)
- unsigned int seed;
- struct random_data *buf;
- {
- int type;
- int32_t *state;
- long int i;
- long int word;
- int32_t *dst;
- int kc;
- if (buf == NULL)
- goto fail;
- type = buf->rand_type;
- if ((unsigned int) type >= MAX_TYPES)
- goto fail;
- state = buf->state;
- /* We must make sure the seed is not 0. Take arbitrarily 1 in this case. */
- if (seed == 0)
- seed = 1;
- state[0] = seed;
- if (type == TYPE_0)
- goto done;
- dst = state;
- word = seed;
- kc = buf->rand_deg;
- for (i = 1; i < kc; ++i)
- {
- /* This does:
- state[i] = (16807 * state[i - 1]) % 2147483647;
- but avoids overflowing 31 bits. */
- long int hi = word / 127773;
- long int lo = word % 127773;
- word = 16807 * lo - 2836 * hi;
- if (word < 0)
- word += 2147483647;
- *++dst = word;
- }
- buf->fptr = &state[buf->rand_sep];
- buf->rptr = &state[0];
- kc *= 10;
- while (--kc >= 0)
- {
- int32_t discard;
- (void) __random_r (buf, &discard);
- }
- done:
- return 0;
- fail:
- return -1;
- }
- int
- rand ()
- {
- return (int) __random ();
- }
- long int
- __random ()
- {
- int32_t retval;
- __libc_lock_lock (lock);
- (void) __random_r (&unsafe_state, &retval);
- __libc_lock_unlock (lock);
- return retval;
- }
- int
- __random_r (buf, result)
- struct random_data *buf;
- int32_t *result;
- {
- int32_t *state;
- if (buf == NULL || result == NULL)
- goto fail;
- state = buf->state;
- if (buf->rand_type == TYPE_0)
- {
- int32_t val = state[0];
- val = ((state[0] * 1103515245) + 12345) & 0x7fffffff;
- state[0] = val;
- *result = val;
- }
- else
- {
- int32_t *fptr = buf->fptr;
- int32_t *rptr = buf->rptr;
- int32_t *end_ptr = buf->end_ptr;
- int32_t val;
- val = *fptr += *rptr;
- /* Chucking least random bit. */
- *result = (val >> 1) & 0x7fffffff;
- ++fptr;
- if (fptr >= end_ptr)
- {
- fptr = state;
- ++rptr;
- }
- else
- {
- ++rptr;
- if (rptr >= end_ptr)
- rptr = state;
- }
- buf->fptr = fptr;
- buf->rptr = rptr;
- }
- return 0;
- fail:
- __set_errno (EINVAL);
- return -1;
- }
在有些rand函数的实现里面,高位比低位的随机性更强, 所以如果要产生1-10之间的随机数,最好用:
j = 1 + (int) (10.0 *(rand() / (RAND_MAX + 1.0)));
而不要用:
j = 1 + (rand() %10);
4. 多线程安全版本
rand不是多线程安全也不是可重入的,在多线程环境下,给定同样的种子,跑出来的序列可能是不一样的,因为实现里面用到了静态变量,在多线程环境下应该使用rand_r函数 (windows上使用rand_s), 下面是glibc的rand_r实现:
[cpp] view plaincopy
- int
- rand_r (unsigned int *seed)
- {
- unsigned int next = *seed;
- int result;
- next *= 1103515245;
- next += 12345;
- result = (unsigned int) (next / 65536) % 2048;
- next *= 1103515245;
- next += 12345;
- result <<= 10;
- result ^= (unsigned int) (next / 65536) % 1024;
- next *= 1103515245;
- next += 12345;
- result <<= 10;
- result ^= (unsigned int) (next / 65536) % 1024;
- *seed = next;
- return result;
- }
5. 其他考虑
游戏里面大量的应用到了随机函数,比如彩票,炼器功能,有意思的是玩家对这些随机时间会比较迷信,经常有各种搞笑的说话,比如在某个NPC下面炼器成功概率高啊, 炼器的时候骂人会提高成功率...
这里有个问题,玩家炼器经常采用垫装备的方法,这个方法简单而言就是先拿几个烂的装备等连续炼失败了几次以后再去上一个好装备,这样会不会提高最后那个好装备的成功概率呢?
个人觉得这个和实现相关,假如每次成功的概率都是50%, 如果前面N次序列都是小于50%,那么第N+1次概率是有可能受影响的,也即随机序列里面的元素并不是完全独立,序列之间是有关系,个人觉得通过这种方面在某种程度能提高成功的概率,但是还要考虑到会受游戏中别的玩家的干扰以及实现的时候可能每次使用不同的种子等的影响。
6. 推荐书籍
Numerical Recipes in C:The Art of Scientific Computing Second Edition: 第七章