本文是為了記錄和澄清一個由來已久的關於C語言隨機數生成器的誤解。
目前所看到的所有公開的關於C隨機數生成器的中文資料,都提到經典的線性同餘法( LCG, linear congruential generator),並認為是預設的實現方法。這個說法並不準確。以GCC為例,GLIBC的確實現了線性同餘法,但是實現的程式碼塊分支在日常使用中不會執行到,線性同餘法為C語言預設隨機數生成器的說法已過時。
本文將以GLIBC原始碼為例,結合掌握的文件,做一回搬運工,總結描述一下GLIBC中隨機數生成器的實現。
線性同餘法
線性同餘法,LCG(linear congruential generator),是經典的偽隨機數產生器演算法,速度快,容易理解實現。 LCG 演算法數學上基於公式:
X(n+1) = (a * X(n) + c) % m
其中,各系數為:模m, 係數a, 0 < a < m,增量c, 0 <= c < m,原始值(種子) 0 <= X(0) < m 。其中引數c, m, a比較敏感,或者說直接影響了偽隨機數產生的質量。
GLIBC中對LCG的實現,取a = 1103515245, c = 12345, m = 134217728,即
X(n+1) = (1103515245 * X(n) + 12345) & 2147483647
由於LCG計算簡單,極省記憶體,很適合記憶體和計算資源比較緊張的嵌入式環境。但LCG有一個嚴重的缺陷,即產生的偽隨機數強依賴於上一次生成的隨機數,且重複週期等於隨機範圍,不能用於隨機數要求高的場合。
原因是單狀態生成器在每次rand()呼叫時不會生成完全的偽隨機數,實際做的是以偽隨機的順序遍歷(0~2^31)範圍內的數。意味著當獲取到一個為偽隨機數時,在當前週期內不會再獲取到同一個數,只有在經過2^31次rand()呼叫之後,才會獲取這個數(而且只會獲取到這個數)。
線性累加反饋法
線性累加反饋法,即LAFM(linear additive feedback method),以下是GLIBC使用的線性累加反饋法的流程描述。其中,2147483647 = 2^31 – 1,4294967296 = 2^32. 所有變數都是整數。 對於給定的種子常量s, 初始化序列r0…r33通過以下步驟計算:
- r(0) = s
- r(i) = (16807 * r(i-1) ) % 2147483647 (i = 1…30)
- r(i) = r(i-31) (i = 31…33)
注意數乘16807的結果應該由足夠大的整數型別儲存,避免取模操作之前發生值溢位。r(i-1)在乘法操作已經是32位整數,r(i)計算結果確保是0到2147483646之間的正整數, 即使r(i-1)為負數。
從r34開始的偽隨機序列,通過以下的線性反饋迴圈來計算:
4. r(i) = (r(i-3) + r(i-31)) % 4294967296 (i ≥ 34)
忽略掉r0…r343序列,rand()函式輸出的偽隨機數o(i)為:
5. o(i) = r(i+344) >> 1
r(i+344)的個位數字移除,生成31位隨機數o(i)。
以下為模擬步驟1~4的程式碼:
1 2 3 4 5 6 7 8 9 |
r[0] = seed; for (i=1; i<31; i++) { r[i] = (16807LL * r[i-1]) % 2147483647; if (r[i] < 0) { r[i] += 2147483647; } for (i=31; i<34; i++) r[i] = r[i-31]; for (i=34; i<344; i++) r[i] = r[i-31] + r[i-3]; for (i=344; i<MAX; i++) r[i] = r[i-31] + r[i-3]; |
GLIBC的實現
GLIBC實現了以上兩種演算法。LAFM生成器標記為 TYPE1, TYPE2, TYPE_3 和 TYPE4 型別,LCG 生成器標記為 TYPE0。相比LCG,LAFM生成器預先生成有很多初始狀態,消除了LCG生成器的週期性遍歷的屬性,在同一個週期內,可以多次獲取到相同的隨機數。
為了提高隨機數生成的時間和空間效率,在計算偽隨機序列時GLIBC使用指標指向包含前驅隨機值的陣列,寫法與按上述公式步驟直譯的方式有所不同。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
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; } |
具體使用哪個生成器依賴於初始狀態集合,由initstate()函式生成:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
int __initstate_r (seed, arg_state, n, buf) unsigned int seed; char *arg_state; size_t n; struct random_data *buf; { if (buf == NULL) goto fail; int32_t *old_state = buf->state; if (old_state != NULL) { int old_type = buf->rand_type; if (old_type == TYPE_0) old_state[-1] = TYPE_0; else old_state[-1] = (MAX_TYPES * (buf->rptr - old_state)) + old_type; } int type; if (n >= BREAK_3) type = n < BREAK_4 ? TYPE_3 : TYPE_4; else if (n < BREAK_1) { if (n < BREAK_0) goto fail; type = TYPE_0; } else type = n < BREAK_2 ? TYPE_1 : TYPE_2; int degree = random_poly_info.degrees[type]; int separation = random_poly_info.seps[type]; buf->rand_type = type; buf->rand_sep = separation; buf->rand_deg = degree; int32_t *state = &((int32_t *) arg_state)[1]; /* First location. */ /* Must set END_PTR before srandom. */ buf->end_ptr = &state[degree]; buf->state = state; __srandom_r (seed, buf); state[-1] = TYPE_0; if (type != TYPE_0) state[-1] = (buf->rptr - state) * MAX_TYPES + type; return 0; fail: __set_errno (EINVAL); return -1; } |
總結
LCG生成器在狀態陣列(buf->state)長度為8位元組時才會使用。 狀態陣列長度更大時則會啟用LAFM生成器。通常在使用rand()方法時,會使用srand()設定種子常量,這時狀態陣列預設就是128位元組, 所以實際會啟用LAFM生成器。
最後,以上分析如果有偏差,很可能是作者對資料或程式碼的理解問題,歡迎即時反饋。
參考資料:
- 純線性同餘隨機數生成器介紹
http://www.cnblogs.com/xkfz007/archive/2012/03/27/2420154.html - random/initstate原始碼
https://github.com/lattera/glibc/blob/master/stdlib/random_r.c - glibc rand function implementation
http://stackoverflow.com/questions/18634079/glibc-rand-function-implementation - The GLIBC random number generator
http://www.mscs.dal.ca/~selinger/random/