重新认识 randomseed

原生 Lua 的 randomseed 是不支持浮点数的

Posted by qmsheng on October 27, 2017

在之前的文章中,我写过这样的测试用例:

local seed = 123456
for i=1,2 do
    math.randomseed(seed + (i-1)/10)
    local num = {}
    for j=1,10 do
        table.insert(num, math.random(100))
    end
    print(table.concat(num, ","))
end

运行结果是:

61,66,19,75,44,61,68,1,33,4
61,66,19,75,44,61,68,1,33,4

目的是为了引出 LCG 的缺点,然而这并不能证明 LCG 算法生成的随机数列质量就不够好。这是因为 randomseed 底层调用的是 C 的 srand ,其需要传入的是一个 unsigned int。Lua 的 randomseed 只是对其做了个包装:如果你传入的不是 int,Lua 会对其进行数值修约,再将修约之后的值传递给 srand 调用,也就是说 randomseed 是不支持浮点数的。上面的用例中,其实 seed 是一样的,都是 123456,因而产生的随机数列是一样的。

另外,需要注意的是由于 srand 接受的是一个 int。而 Lua 用的是 double,那么如果你将 randomseed 的 seed 越界,那么其随机性将会被打破。例如:

math.randomseed(2^40)
local num1 = {}
for i=1,10 do
    table.insert(num1, math.random(100))
end
print(table.concat(num1, ","))

math.randomseed(2^52)
local num2 = {}
for i=1,10 do
    table.insert(num2, math.random(100))
end
print(table.concat(num2, ","))

math.randomseed(0)
local num3 = {}
for i=1,10 do
    table.insert(num3, math.random(100))
end
print(table.concat(num3, ","))

运行结果:

85,40,79,80,92,20,34,77,28,56
85,40,79,80,92,20,34,77,28,56
85,40,79,80,92,20,34,77,28,56

问题在于 Lua 的 randomseed 即使发生越界,也并不会报错,甚至连一个警告都没有。所以需要我们自己小心的处理这个问题,以免留下隐患。比如可以像下面这样对其重写:

-- fixed the overflow of randomseed by checking on the Lua side
math.randomseed = function(seed)
    local bitsize = 32
    local lua_version = tonumber(_VERSION:match("%d%.*%d*"))

    local seed = math.floor(math.abs(seed))
    if seed >= (2^bitsize) then
        -- integer overflow, so reduce to prevent a bad seed
        seed = seed - math.floor(seed / 2^bitsize) * (2^bitsize)
    end
    if lua_version < 5.2 then
        -- 5.1 uses (incorrect) signed int
        math.randomseed(seed - 2^(bitsize-1))
    else
        -- 5.2 uses (correct) unsigned int
        math.randomseed(seed)
    end

    return seed
end

这里需要说明下,Lua 5.1 randomseed 用的其实是一个 signed int,并不是 unsigned int,这个问题在 5.2 中已经得到了修正。来源 lua-wiki

Another thing to be aware of is truncation of the seed provided. math.randomseed will call the underlying C function srand which takes an unsigned integer value. Lua will cast the value of the seed to this format. In case of an overflow the seed will actually become a bad seed, without warning [3] (note that Lua 5.1 actually casts to a signed int [4], which was corrected in 5.2).

最后,又一个好消息是:LuaJIT 的 randomseed 是支持浮点数(double)的。其并没有调用 srand,而是利用自己的 PRNG 算法实现的。赞一个 :-)

The PRNG generates the same sequences from the same seeds on all platforms and makes use of all bits in the seed argument. math.random() without arguments generates 52 pseudo-random bits for every call. The result is uniformly distributed between 0.0 and 1.0. It’s correctly scaled up and rounded for math.random(n [,m]) to preserve uniformity.