这是一个 Park-Miller 伪随机数生成器:
def gen1(a=783):
while True:
a = (a * 48271) % 0x7fffffff
yield a
这783
只是一个任意的种子。这48271
是 Park 和 Miller 在原始论文中推荐的系数(PDF:Park, Stephen K.;Miller, Keith W. (1988). “Random Number Generators: Good Ones are Hard To Find”)
我想提高这个LCG的性能。文献描述了一种使用按位技巧(来源)避免除法的方法:
素数模数需要计算双倍宽度乘积和显式缩减步骤。如果使用的模数刚好小于 2 的幂(梅森素数 2 31 -1 和 2 61 -1 很流行,2 32 -5 和 2 64 -59 也是如此),减少模 m = 2 e - d 可以比使用恒等式 2 e ≡ d (mod m)的一般双宽度除法更便宜。
注意到模数0x7fffffff
实际上是梅森素数 2**32 - 1,这是用 Python 实现的想法:
def gen2(a=783):
while True:
a *= 48271
a = (a & 0x7fffffff) + (a >> 31)
a = (a & 0x7fffffff) + (a >> 31)
yield a
基本基准测试脚本:
import time, sys
g1 = gen1()
g2 = gen2()
for g in g1, g2:
t0 = time.perf_counter()
for i in range(int(sys.argv[1])): next(g)
print(g.__name__, time.perf_counter() - t0)
在 pypy (7.3.0 @ 3.6.9) 中性能得到了改进,例如生成 100 M 项:
$ pypy lcg.py 100000000
gen1 0.4366550260456279
gen2 0.3180829349439591
不幸的是,在 CPython (3.9.0 / Linux) 中性能实际上有所下降:
$ python3 lcg.py 100000000
gen1 20.650125587941147
gen2 26.844335232977755
我的问题:
- 为什么通常被吹捧为优化的按位算术实际上比 CPython 中的模运算还要慢?
- 您能否以其他方式在 CPython 下提高此 PRNG 的性能,也许使用 numpy 或ctypes?
请注意,此处不一定需要任意精度整数,因为此生成器永远不会产生长于:
>>> 0x7fffffff.bit_length()
31