python 2 对比 python 3 随机性能,尤其是 `random.sample` 和 `random.shuffle`

python 2 vs python 3 performance of random, particularly `random.sample` and `random.shuffle`

python 随机模块的性能问题,特别是 random.samplerandom.shuffle 中出现。在我的电脑上,我得到以下结果:

> python  -m timeit -s 'import random' 'random.randint(0,1000)'
1000000 loops, best of 3: 1.07 usec per loop
> python3 -m timeit -s 'import random' 'random.randint(0,1000)'
1000000 loops, best of 3: 1.3 usec per loop

python3 与 python2 相比,性能下降超过 20%。情况变得更糟。

> python  -m timeit -s 'import random' 'random.shuffle(list(range(10)))'
100000 loops, best of 3: 3.85 usec per loop
> python3 -m timeit -s 'import random' 'random.shuffle(list(range(10)))'
100000 loops, best of 3: 8.04 usec per loop

> python  -m timeit -s 'import random' 'random.sample(range(10),3)'
100000 loops, best of 3: 2.4 usec per loop
> python3 -m timeit -s 'import random' 'random.sample(range(10),3)'
100000 loops, best of 3: 6.49 usec per loop

这表示 random.shuffle 的性能下降了 100%,而 random.sample 的性能几乎下降了 200%。太厉害了。


我在上面的测试中使用了 python 2.7.9 和 python 3.4.2。

我的问题:python3 中的 random 模块发生了什么?

------------ 改变了什么 -------------------------- ------------------

发生了几件事:

  • 整数在 int/long 统一中的效率降低了。这也是为什么整数现在是 28 字节宽,而不是 64 位 Linux/MacOS 构建中的 24 字节。

  • 通过使用 _randbelow,随机播放变得更加准确。这消除了先前算法中的细微偏差。

  • 索引变得更慢,因为整数索引的特殊情况已从 ceval.c 中删除,主要是因为它更难处理较新的整数和因为一些核心开发人员认为优化不值得。

  • range() 函数已替换为 xrange()。这是相关的,因为 OP 的计时在内部循环中都使用 range()

shuffle()sample() 的算法在其他方面没有改变。

Python 3 进行了一些更改,例如 unicode-everywhere,使内部结构更复杂、速度更慢,并且内存更密集。在return中,Python 3 使用户更容易编写正确的代码。

同样,int/long 统一使语言更简单,但以速度和 space 为代价。在 random 模块中切换到使用 _randbelow() 有运行时成本,但在准确性和正确性方面受益。

------------结论---------------------------- ----------------------

简而言之,Python 3 在某些对许多用户很重要的方面更好,而在某些人们很少注意到的方面更差。工程通常是权衡取舍。

------------ 详情---------------------------- ----------------------------

Python2.7 代码 shuffle():

def shuffle(self, x, random=None):
    if random is None:
        random = self.random
    _int = int
    for i in reversed(xrange(1, len(x))):
        # pick an element in x[:i+1] with which to exchange x[i]
        j = _int(random() * (i+1))
        x[i], x[j] = x[j], x[i]

Python3.6 代码 shuffle():

def shuffle(self, x, random=None):
    if random is None:
        randbelow = self._randbelow
        for i in reversed(range(1, len(x))):
            # pick an element in x[:i+1] with which to exchange x[i]
            j = randbelow(i+1)              # <-- This part changed
            x[i], x[j] = x[j], x[i]
    else:
        _int = int
        for i in reversed(range(1, len(x))):
            # pick an element in x[:i+1] with which to exchange x[i]
            j = _int(random() * (i+1))
            x[i], x[j] = x[j], x[i]

Python 2.7整数大小:

>>> import sys
>>> sys.getsizeof(1)
24

Python 3.6整数大小:

>>> import sys
>>> sys.getsizeof(1)
28

索引查找的相对速度(带有整数参数索引到列表中的二进制订阅):

$ python2.7 -m timeit -s 'a=[0]' 'a[0]'
10000000 loops, best of 3: 0.0253 usec per loop
$ python3.6 -m timeit -s 'a=[0]' 'a[0]'
10000000 loops, best of 3: 0.0313 usec per loop

Python ceval.c 中的 2.7 代码对索引查找进行了优化:

    TARGET_NOARG(BINARY_SUBSCR)
    {
        w = POP();
        v = TOP();
        if (PyList_CheckExact(v) && PyInt_CheckExact(w)) {
            /* INLINE: list[int] */
            Py_ssize_t i = PyInt_AsSsize_t(w);
            if (i < 0)
                i += PyList_GET_SIZE(v);
            if (i >= 0 && i < PyList_GET_SIZE(v)) {
                x = PyList_GET_ITEM(v, i);
                Py_INCREF(x);
            }
            else
                goto slow_get;
        }
        else
          slow_get:
            x = PyObject_GetItem(v, w);
        Py_DECREF(v);
        Py_DECREF(w);
        SET_TOP(x);
        if (x != NULL) DISPATCH();
        break;
    }

Python ceval.c 中的 3.6 代码没有索引查找的优化:

    TARGET(BINARY_SUBSCR) {
        PyObject *sub = POP();
        PyObject *container = TOP();
        PyObject *res = PyObject_GetItem(container, sub);
        Py_DECREF(container);
        Py_DECREF(sub);
        SET_TOP(res);
        if (res == NULL)
            goto error;
        DISPATCH();
    }