1. 程式人生 > >lua math.random偽隨機問題淺析

lua math.random偽隨機問題淺析

targe ensure table keep 當前系統時間 get eve ons sig

在lua中,如果我們需要隨機數的時候,會使用到math.random,為了避免偽隨機我們的一般編寫方式如下:

-- 獲取當前系統時間(秒)作為隨機種子
math.randomseed(os.time())

-- 有三種方式:
-- 1. 不帶參數調用時,獲取的是[0,1)範圍內的隨機浮點數
-- 2. 帶一個整型參數時,獲取的是[1,n]範圍內的隨機整數
-- 3. 帶兩個整型參數m,n時,獲取的是[m,n]範圍內隨機整數
-- 請註意Lua5.3以後,參數一定要為整數,否則會返回錯誤:bad argument #1 to ‘random‘ (number has no integer representation)
math.random(10, 30)

為何避免偽隨機,我們為何要使用os.time()獲取系統時間秒數作為種子呢?接下來我們將從lua進入c中一層層的random,randomseed的實現慢慢剝離出來。

lua C庫相關文件的官方下載地址:http://www.lua.org/ftp/

下載成功後,其相關文件在src目錄中,我們可以查看lmathlib.c文件

或者查看lua 源碼相關,其地址為:http://www.lua.org/source/5.1/

static const luaL_Reg mathlib[] = {
  {"abs",   math_abs},
  {"acos",  math_acos},
  {
"asin", math_asin}, {"atan", math_atan}, {"ceil", math_ceil}, {"cos", math_cos}, {"deg", math_deg}, {"exp", math_exp}, {"tointeger", math_toint}, {"floor", math_floor}, {"fmod", math_fmod}, {"ult", math_ult}, {"log", math_log}, {"max", math_max}, {"min", math_min}, {
"modf", math_modf}, {"rad", math_rad}, {"random", math_random},    // 關於math.random的調用方法:math_random {"randomseed", math_randomseed},  // 關於math.randomseed的調用方法:math_randomseed {"sin", math_sin}, {"sqrt", math_sqrt}, {"tan", math_tan}, {"type", math_type}, /* placeholders */ {"pi", NULL}, {"huge", NULL}, {"maxinteger", NULL}, {"mininteger", NULL}, {NULL, NULL} };

接下來我們查看下關於math_random,math_randomseed的實現:

/*
** This function uses ‘double‘ (instead of ‘lua_Number‘) to ensure that
** all bits from ‘l_rand‘ can be represented, and that ‘RANDMAX + 1.0‘
** will keep full precision (ensuring that ‘r‘ is always less than 1.0.)
*/
static int math_random (lua_State *L) {
  lua_Integer low, up;
  double r = (double)l_rand() * (1.0 / ((double)L_RANDMAX + 1.0));
  switch (lua_gettop(L)) {  /* check number of arguments */
    case 0: {  /* no arguments */
      lua_pushnumber(L, (lua_Number)r);  /* Number between 0 and 1 */
      return 1;
    }
    case 1: {  /* only upper limit */
      low = 1;
      up = luaL_checkinteger(L, 1);
      break;
    }
    case 2: {  /* lower and upper limits */
      low = luaL_checkinteger(L, 1);
      up = luaL_checkinteger(L, 2);
      break;
    }
    default: return luaL_error(L, "wrong number of arguments");
  }
  /* random integer in the interval [low, up] */
  luaL_argcheck(L, low <= up, 1, "interval is empty");
  luaL_argcheck(L, low >= 0 || up <= LUA_MAXINTEGER + low, 1,
                   "interval too large");
  r *= (double)(up - low) + 1.0;
  lua_pushinteger(L, (lua_Integer)r + low);
  return 1;
}

static int math_randomseed (lua_State *L) {
  l_srand((unsigned int)(lua_Integer)luaL_checknumber(L, 1));
  (void)l_rand(); /* discard first value to avoid undesirable correlations */
  return 0;
}

關於l_rand, l_srand的實現參考代碼:

#if !defined(l_rand
#if defined(LUA_USE_POSIX)
#define l_rand()    random()
#define l_srand(x)    srandom(x)
#define L_RANDMAX    2147483647    /* (2^31 - 1), following POSIX */
#else
// windows平臺等
#define l_rand()    rand()
#define l_srand(x)    srand(x)
#define L_RANDMAX    RAND_MAX         // 0x7fff
#endif
#endif

到此處,我們應該就明了,lua隨機數的實現,來自於stdlib.h文件下的rand,srand. 那實現的方法如下:

// 參考來自:The Standard C Library 中第337頁
static
unsigned long int next = 1; /* RAND_MAX assumed to be 32767 */ int rand(void) { next = next * 1103515245 + 12345; return((unsigned)(next/65536) % 32768); } void srand(unsigned seed) { next = seed; }

看到如上代碼,我們應該明白,在沒有設定randomseed(即srand)的情況下,我們的隨機種子next默認為1,隨機種子數值越小越容易導致隨機數計算得出的數值集中,比如在lua中我們編寫這樣的程序:

-- 隨機種子數值為100,每次執行的結果都是:1 1 1 3 4 2 4 1 4 1
math.randomseed(100)
-- 隨機種子數字為os.time(),每次執行結果會發生改變
math.randomseed(os.time())
local randNum = {}
for i = 1,10 do
    table.insert(randNum,math.random(1,5))
end
print(table.concat(randNum," "))

到此刻,通過獲取系統時間的秒值來設定隨機種子,似乎偽隨機的問題已經解決了。但是如果你的程序在1秒內有多次操作,產生的隨機數還會是偽隨機。

為了避免這種問題出現,前輩們會將隨機種子的數值設置為毫秒級,甚至微秒級的數值來處理問題。或者

-- 將os.time()獲取的系統秒數數值翻轉(低位變高位),再取高6位,這樣即使time變化很小
-- 由於低位變高位,數值就會變化很大,這樣1秒內進行多次運行的話,效果會好些
local next = tostring(os.time()):reverse():sub(1, 6)
math.randomseed(next )

lua math.random偽隨機問題淺析