【发布时间】:2021-01-26 21:14:51
【问题描述】:
这是一个 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 的性能。文献描述了一种使用按位技巧来避免除法的方法 (source):
素数模数需要计算双倍宽度乘积和显式缩减步骤。如果使用的模数刚好小于 2 的幂(梅森素数 231-1 和 261-1 很流行,232-5 和 264-59),减少模 m = 2e - 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 中的模运算还要慢?
- 您能否通过其他方式提高此 PRNG 在 CPython 下的性能,例如使用 numpy 或 ctypes?
请注意,此处不一定需要任意精度整数,因为此生成器生成的数字永远不会超过:
>>> 0x7fffffff.bit_length()
31
【问题讨论】:
-
我怀疑对于 CPython,你最好的选择可能是
numpy和numba,或者只是创建一个 C 扩展(也许是 Cython)。 -
使用内置
from random import getrandbits,我得到了大约 4 倍的结果。 IPython 的%timeit getrandbits(31)是 50ns,而您的gen1()是 210ns。我构建了 gen1() 和 gen2() 的 C 版本并使用 ctypes 调用,但 gen1() 的速度相同,而 gen2() 仍然较慢。 -
@MarkTolonen 实际上,不是在寻找任何旧的随机数生成器。我特别需要处理这个周期性序列。我想知道 pypy 是如何比我使用 ctypes、numpy 和/或 numba 实现的更好地 jit...
-
想知道 pypy 是如何比我使用 ctypes、numpy 和/或 numba 更好地实现 jit 的 - CPython 大部分时间都花在解释器上,查看字节码以及类型。由于“棘手”的代码有更多的步骤,解释器的开销会消耗掉您从消除整数除法中获得的收益。当然,JIT 编译器也会为棘手的编译器生成更长的代码,但是到那时,DIV/IDIV 确实是一条慢指令确实很重要(而移位/添加/和指令通常只需要一个周期)。
-
我是建议尝试一个原生模块,但我不确定它在这几个步骤中是否获得足够的收益。 Dupe-ish 主题stackoverflow.com/questions/9009139/… 以这个建议结束,但没有确凿的事实或衡量标准。大肆宣传的答案对于 32 位数字来说完全是题外话。
标签: python numpy optimization ctypes lcg