簡體   English   中英

為什么 random.randint() 比 random.getrandbits() 慢得多

[英]Why random.randint() is much slower than random.getrandbits()

A 做了一個測試,比較了 Python 中的random.randint()random.getrandbits() 結果表明getrandbitsrandint

from random import randint, getrandbits, seed
from datetime import datetime

def ts():
    return datetime.now().timestamp()

def diff(st, en):
    print(f'{round((en - st) * 1000, 3)} ms')

seed(111)
N = 6

rn = []
st = ts()
for _ in range(10**N):
    rn.append(randint(0, 1023))
en = ts()
diff(st,en)

rn = []
st = ts()
for _ in range(10**N):
    rn.append(getrandbits(10))
en = ts()
diff(st,en)

數字范圍完全相同,因為10隨機位的范圍從0 (在0000000000的情況下)到1023 (在1111111111的情況下)。

output的代碼:

590.509 ms
91.01 ms

可以看到,相同條件下的getrandbits幾乎快了6.5倍。 但為什么? 有人可以解釋一下嗎?

randint使用randrange ,它使用_randbelow ,即_randbelow_with_get_randbits (如果存在),它使用getrandbits 但是, randrange包括對getrandbits的基礎調用之上的開銷,因為它必須檢查開始和步驟 arguments。 即使有這些檢查的快速路徑,它們仍然存在。 使用randint時肯定還有其他因素導致 6.5 減速,但這個答案至少向您表明randint總是比getrandbits慢。

getrandbits()是一個非常短的 function 幾乎立即進入本機代碼:

def getrandbits(self, k):
    """getrandbits(k) -> x.  Generates an int with k random bits."""
    if k < 0:
        raise ValueError('number of bits must be non-negative')
    numbytes = (k + 7) // 8                       # bits / 8 and rounded up
    x = int.from_bytes(_urandom(numbytes), 'big')
    return x >> (numbytes * 8 - k)                # trim excess bits

_urandomos.urandom ,您甚至無法在同一文件夾中的os.py中找到它,因為它是本機代碼。 在此處查看詳細信息: 我在哪里可以找到 os.urandom() 的源代碼?

randint()是一個return self.randrange(a, b+1)調用,其中randrange()是一個長而通用的 function,將近 100 行 Python 代碼,它的“快速通道”以return istart + self._randbelow(width)經過大約 50 行檢查和初始化。

_randbelow()實際上是一種選擇,具體取決於您的情況中是否存在getrandbits() ,所以可能this正在運行:

def _randbelow_with_getrandbits(self, n):
    "Return a random int in the range [0,n).  Returns 0 if n==0."

    if not n:
        return 0
    getrandbits = self.getrandbits
    k = n.bit_length()  # don't use (n-1) here because n can be 1
    r = getrandbits(k)  # 0 <= r < 2**k
    while r >= n:
        r = getrandbits(k)
    return r

所以randint() getrandbits()結束,這幾乎就是它比它慢的原因。

The underlying reason is the function that getrandbits calls is written in C , look at the source code at GitHub Python repository

你會發現getrandbits在后台調用了os模塊中的urandom ,如果你看一下這個function的實現,你會發現它確實是用C寫的。

os_urandom_impl(PyObject *module, Py_ssize_t size)
/*[clinic end generated code: output=42c5cca9d18068e9 input=4067cdb1b6776c29]*/
{
    PyObject *bytes;
    int result;

    if (size < 0)
        return PyErr_Format(PyExc_ValueError,
                            "negative argument not allowed");
    bytes = PyBytes_FromStringAndSize(NULL, size);
    if (bytes == NULL)
        return NULL;

    result = _PyOS_URandom(PyBytes_AS_STRING(bytes), PyBytes_GET_SIZE(bytes));
    if (result == -1) {
        Py_DECREF(bytes);
        return NULL;
    }
    return bytes;
}

說到randint ,如果你看一下這個 function 的源代碼,它調用randrange ,它進一步調用_randbelow_with_get_randbits

    def randrange(self, start, stop=None, step=_ONE):
        """Choose a random item from range(start, stop[, step]).
        This fixes the problem with randint() which includes the
        endpoint; in Python this is usually not what you want.
        """

        # This code is a bit messy to make it fast for the
        # common case while still doing adequate error checking.
        try:
            istart = _index(start)
        except TypeError:
            istart = int(start)
            if istart != start:
                _warn('randrange() will raise TypeError in the future',
                      DeprecationWarning, 2)
                raise ValueError("non-integer arg 1 for randrange()")
            _warn('non-integer arguments to randrange() have been deprecated '
                  'since Python 3.10 and will be removed in a subsequent '
                  'version',
                  DeprecationWarning, 2)
        if stop is None:
            # We don't check for "step != 1" because it hasn't been
            # type checked and converted to an integer yet.
            if step is not _ONE:
                raise TypeError('Missing a non-None stop argument')
            if istart > 0:
                return self._randbelow(istart)
            raise ValueError("empty range for randrange()")

        # stop argument supplied.
        try:
            istop = _index(stop)
        except TypeError:
            istop = int(stop)
            if istop != stop:
                _warn('randrange() will raise TypeError in the future',
                      DeprecationWarning, 2)
                raise ValueError("non-integer stop for randrange()")
            _warn('non-integer arguments to randrange() have been deprecated '
                  'since Python 3.10 and will be removed in a subsequent '
                  'version',
                  DeprecationWarning, 2)
        width = istop - istart
        try:
            istep = _index(step)
        except TypeError:
            istep = int(step)
            if istep != step:
                _warn('randrange() will raise TypeError in the future',
                      DeprecationWarning, 2)
                raise ValueError("non-integer step for randrange()")
            _warn('non-integer arguments to randrange() have been deprecated '
                  'since Python 3.10 and will be removed in a subsequent '
                  'version',
                  DeprecationWarning, 2)
        # Fast path.
        if istep == 1:
            if width > 0:
                return istart + self._randbelow(width)
            raise ValueError("empty range for randrange() (%d, %d, %d)" % (istart, istop, width))

        # Non-unit step argument supplied.
        if istep > 0:
            n = (width + istep - 1) // istep
        elif istep < 0:
            n = (width + istep + 1) // istep
        else:
            raise ValueError("zero step for randrange()")
        if n <= 0:
            raise ValueError("empty range for randrange()")
        return istart + istep * self._randbelow(n)

    def randint(self, a, b):
        """Return random integer in range [a, b], including both end points.
        """

        return self.randrange(a, b+1)

正如你所看到的,在randrange function 和 Python 中的Try / Except塊中發生了很多事情,由於所有這些開銷和控制流,它很慢。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM