【问题标题】:Why (in Python) is random.randint so much slower than random.random?为什么(在 Python 中)random.randint 比 random.random 慢得多?
【发布时间】:2019-09-26 20:54:06
【问题描述】:

我对一些随机整数生成代码的相对速度感到好奇。我写了以下内容来检查一下:

from random import random
from random import choice
from random import randint
from math import floor
import time

def main():
    times = 1000000
    
    startTime = time.time()
    for i in range(times):
        randint(0,9)
    print(time.time()-startTime)
    
    startTime = time.time()
    for i in range(times):
        choice([0,1,2,3,4,5,6,7,8,9])
    print(time.time()-startTime)
    
    startTime = time.time()
    for i in range(times):
        floor(10*random())##generates random integers in the same range as randint(0,9)
    print(time.time()-startTime)

main()

这段代码的一次试验结果是

0.9340872764587402

0.6552846431732178

0.23188304901123047

即使在执行了乘法和 math.floor 之后,生成整数的最终方法也是迄今为止最快的。弄乱生成数字的范围大小并没有改变任何东西。

那么,为什么 random 方式比 randint 快?是否有任何理由(除了易用性、可读性和不引起错误)人们更喜欢 randint 而不是随机(例如,randint 产生更多随机伪随机整数)?如果floor(x*random()) 感觉可读性不够,但您想要更快的代码,您是否应该使用专门的例程?

def myrandint(low,high):   ###still about 1.6 longer than the above, but almost 2.5 times faster than random.randint
    return floor((high-low+1)*random())+low  ##returns a random integer between low and high, inclusive. Results may not be what you expect if int(low) != low, etc. But the numpty who writes 'randint(1.9,3.2)' gets what they deserve.
  

【问题讨论】:

  • randint 在底层使用randrange,它有一个带有大量开销的python 实现:github.com/python/cpython/blob/master/Lib/random.py#L211 基本上在每次调用该函数时,它都会进行大量错误检查。如果你不需要这些,你当然可以使用更简单的实现。
  • 使用floor(n*random()) 计算[0, n) 中的整数存在偏差。对于n=10,这种偏差在统计上是无法检测到的,但对于较大的n,可能会出现问题。例如,请参阅 bugs.python.org/issue9025 进行一些讨论。

标签: python random


【解决方案1】:

在我回答您的问题之前(别担心,我确实做到了),请注意常见的程序员习语:

过早的优化是万恶之源。

this isn't always the case 时,除非您需要,否则不要担心微优化。

这对于 Python 来说是双倍的:如果您正在编写速度至关重要的东西,您通常会希望使用运行速度更快的语言来编写它,例如 C。然后,您可以为该 C 代码编写 Python 绑定,如果您想要将 Python 用于应用程序的非关键部分(例如 NumPy)。

与其专注于使代码中的单个表达式或函数尽可能快地运行,不如专注于您使用的算法和代码的整体结构(并使其具有可读性,但您已经意识到这一点)。然后,当您的应用程序开始运行缓慢时,您可以对其进行分析以找出哪些部分花费的时间最多,并仅改进这些部分。

对结构良好、可读性强的代码进行更改将更容易,并且优化实际瓶颈通常会比大多数微优化提供更好的加速与时间编码比率。花在思考两个表达式中哪个运行得更快的时间是您本可以完成其他事情的时间。

作为一个例外,我会说为什么学习一个选项比另一个更快是值得的,因为这样你就可以将更多的常识融入你未来的编程中,让你更快速的通话,无需担心细节。

但是关于为什么我们不应该浪费时间担心速度已经足够了,让我们来谈谈速度。


看看the source of the random module(对于CPython 3.7.4),开头评论末尾的这一行提供了一个简短的答案:

* The random() method is implemented in C, executes in a single Python step,
  and is, therefore, threadsafe.

第一个陈述是对我们最重要的陈述。 random 是一个 C 函数的 python 绑定,所以它的操作复杂度以机器码的速度运行,而不是 Python 的相对慢的速度。

randint,另一方面,在 Python 中实现的,因此会受到显着的速度损失。 randint 调用 randrange,确保范围的边界(和步长)是整数,范围不为空,步长不为零,然后调用 getrandbits,实现于C.

仅此一项就导致了randint 的大部分缓慢。但是,还有一个变量在起作用。

再深入一点,进入内部函数_randbelow,结果发现获取0到n之间的随机数的算法非常简单:获取n中的位数,然后生成随机重复这么多位,直到结果数不大于n

平均而言(在n 的所有可能值中),这几乎没有影响,但比较极端情况时,这是很明显的。

我写了a function 来测试该循环的影响。结果如下:

bits   2 ** (n - 1)   (2 ** n) - 1   ratio
  64   1.358526759    1.084741422    1.2523968675
 128   1.43073282     1.02119227     1.4010415688
 256   1.600253063    1.271662798    1.2583941793
 512   1.845024581    1.363168823    1.3534820852
1024   2.371779281    1.620392686    1.4637064839
2048   2.98949864     2.01788896     1.48149809

第一列是位数,第二列和第三列是在 1 000 000 次运行中找到具有这么多位的随机整数的平均时间(以微秒为单位)。最后一列是第二列和第三列的比例。

您会注意到,具有给定位长度的最大数字的平均运行时间大于具有该位长度的最小数字的平均运行时间。这是因为那个循环:

当查找小于最大 n 位数的 n 位数时,仅当生成最大数时才需要第二次尝试,这不太可能除了非常小的 n。但是要找到一个小于最小的数(2n-1 是单个 1 位,后跟 n-1 个 0 位) ,一半的尝试失败。


附录:我删除了位长度从 1 到 32 的测试,因为在检查 getrandbits 的 C 源代码后,我发现它为这些数字使用了一个单独的、更快的函数。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-10-11
    • 1970-01-01
    • 2018-09-19
    • 1970-01-01
    • 2017-12-26
    • 2011-01-25
    相关资源
    最近更新 更多