为什么在Python 3中,x ** 4.0比x ** 4更快?

166

为什么在CPython 3.5.2中x ** 4.0x ** 4快?

$ python -m timeit "for x in range(100):" " x**4.0"
  10000 loops, best of 3: 24.2 usec per loop

$ python -m timeit "for x in range(100):" " x**4"
  10000 loops, best of 3: 30.6 usec per loop

我尝试更改指数以观察其行为。例如,如果我将x的指数提高到10或16,它会从30跳到35。但是,如果我使用浮点数 10.0 作为指数,则只会在24.1〜4之间移动。

我猜这可能与浮点数转换和2的幂有关,但我不确定。

我注意到,在这两种情况下,2的幂都比较快,我猜这是因为这些计算对解释器/计算机来说更本地/更容易。但是,使用浮点数几乎不会移动。2.0 => 24.1~4 & 128.0 => 24.1~4但是2 => 29 & 128 => 62


TigerhawkT3指出,这种情况不会发生在循环之外。我检查后发现,只有当底数被提高时才会出现这种情况(据我所见)。对此有什么想法吗?


11
就性价比而言:对我来说,Python 2.7.13 的速度是 Python 3.x 的两到三倍,而且它表现出相反的行为:整数指数比浮点数指数更快。 - user707650
4
@Evert 是的,x**4.0 我得到了14微秒,而 x**4 得到了3.9微秒。 - dabadaba
3个回答

164
以下是翻译内容:

为什么在Python 3中,x**4.0x**4更快*

Python 3的int对象是一个完整的对象,旨在支持任意大小;由于这个事实,它们在C级别上被视为这样处理(请参见long_pow中声明所有变量都是PyLongObject *类型)。这也使得它们的指数运算更加{{em>棘手和繁琐,因为你需要在使用ob_digit数组表示其值时进行调整。(勇敢者可参考源代码。 -- 有关PyLongObject的更多信息,请参见了解Python中大整数内存分配。)

Python的float对象,相反地,可以通过使用PyFloat_AsDouble将其转换为C double类型,并且可以使用这些本机类型执行操作。这非常好,因为在检查相关边缘情况后,它允许Python 使用平台的pow也就是C的pow)来处理实际的指数运算:

/* Now iv and iw are finite, iw is nonzero, and iv is
 * positive and not equal to 1.0.  We finally allow
 * the platform pow to step in and do the rest.
 */
errno = 0;
PyFPE_START_PROTECT("pow", return NULL)
ix = pow(iv, iw); 

其中iviw是我们原始的PyFloatObject,作为C中的double

值得一提的是:对我而言,Python 2.7.13更快了2~3倍,并显示出相反的行为。

前面的事实也解释了Python 2和3之间的差异,因此,我认为这个评论也很有趣,需要解释一下。

在Python 2中,您使用的是旧的int对象,它与Python 3中的int对象不同(所有3.x中的int对象都是PyLongObject类型)。在Python 2中,这取决于对象的值(或者,如果使用后缀L/l):

# Python 2
type(30)  # <type 'int'>
type(30L) # <type 'long'>

你在这里看到的<type 'int'>float做相同的事情,当进行指数运算时,它会被安全地转换为C语言中的long(如果可以的话,int_pow还会提示编译器将其放入寄存器中,这可能会有所不同)

static PyObject *
int_pow(PyIntObject *v, PyIntObject *w, PyIntObject *z)
{
    register long iv, iw, iz=0, ix, temp, prev;
/* Snipped for brevity */    

这可以获得很好的速度提升。
为了看到在比较中<type 'long'><type 'int'>的迟缓程度,如果你在Python 2中使用long调用来包装x名称(实质上强制它使用像Python 3中的long_pow),速度增益就会消失:
# <type 'int'>
(python2) ➜ python -m timeit "for x in range(1000):" " x**2"       
10000 loops, best of 3: 116 usec per loop
# <type 'long'> 
(python2) ➜ python -m timeit "for x in range(1000):" " long(x)**2"
100 loops, best of 3: 2.12 msec per loop

请注意,虽然其中一个代码片段将 int 转换为 long,而另一个则没有(正如 @pydsinger 指出的那样),但这种转换并不是减速的主要原因。实现 long_pow 的方式才是。(只使用 long(x) 来计时语句以查看结果)。

[...] 它不会在循环之外发生。[...] 有任何想法吗?

这是 CPython 的 peephole 优化器为您折叠常量。由于没有实际计算来找到指数运算的结果,只有值的加载,因此两种情况下的时间完全相同。
dis.dis(compile('4 ** 4', '', 'exec'))
  1           0 LOAD_CONST               2 (256)
              3 POP_TOP
              4 LOAD_CONST               1 (None)
              7 RETURN_VALUE

对于'4 ** 4.'生成的字节码是相同的,唯一的区别在于LOAD_CONST加载的是浮点数256.0而不是整数256

dis.dis(compile('4 ** 4.', '', 'exec'))
  1           0 LOAD_CONST               3 (256.0)
              2 POP_TOP
              4 LOAD_CONST               2 (None)
              6 RETURN_VALUE

所以时间是相同的。

*以上所有内容仅适用于Python的参考实现CPython。其他实现可能表现不同。


无论是什么,它都与在“range”上进行循环有关,因为仅计时“**”操作本身在整数和浮点数之间没有差异。 - TigerhawkT3
1
但是,常量将被折叠@TigerhawkT3(dis(compile('4 ** 4', '', 'exec'))),因此时间应该完全相同 - Dimitris Fasarakis Hilliard
你最后的时间似乎不符合你所说的。long(x)**2. 相比于 long(x)**2 仍然快了4-5倍。(尽管我不是downvoters之一) - Graipher
1
那么,如果 Python 3 改动会带来负面的速度影响并且不能再使用整型操作的本地类型,为什么还要这样做呢? - mbomb007
3
在Python 3中淘汰<type 'long'>类型可能是为了简化语言所做的努力。如果您可以有一种类型表示整数,那么比两种更易管理(并且在必要时担心从一种转换为另一种,用户感到困惑等)。速度增益次要。PEP 237的理论部分也提供了更多见解。 - Dimitris Fasarakis Hilliard
显示剩余8条评论

25

如果我们看一下字节码,就会发现表达式是完全相同的。唯一的区别在于一个常量的类型,这个常量将成为BINARY_POWER的一个参数。因此,最有可能是因为在后面将int转换为浮点数。

>>> def func(n):
...    return n**4
... 
>>> def func1(n):
...    return n**4.0
... 
>>> from dis import dis
>>> dis(func)
  2           0 LOAD_FAST                0 (n)
              3 LOAD_CONST               1 (4)
              6 BINARY_POWER
              7 RETURN_VALUE
>>> dis(func1)
  2           0 LOAD_FAST                0 (n)
              3 LOAD_CONST               1 (4.0)
              6 BINARY_POWER
              7 RETURN_VALUE

更新:让我们来看看CPython源代码中的Objects/abstract.c

PyObject *
PyNumber_Power(PyObject *v, PyObject *w, PyObject *z)
{
    return ternary_op(v, w, z, NB_SLOT(nb_power), "** or pow()");
}

PyNumber_Power 调用 ternary_op,这个函数很长,不方便在此处贴出,所以这里是链接

它调用了 xnb_power 插槽,并将 y 作为参数传递。

最终,在位于 Objects/floatobject.c 的第 686 行的 float_pow() 中,我们看到在实际操作之前,参数被转换为 C 的 double 类型。

static PyObject *
float_pow(PyObject *v, PyObject *w, PyObject *z)
{
    double iv, iw, ix;
    int negate_result = 0;

    if ((PyObject *)z != Py_None) {
        PyErr_SetString(PyExc_TypeError, "pow() 3rd argument not "
            "allowed unless all arguments are integers");
        return NULL;
    }

    CONVERT_TO_DOUBLE(v, iv);
    CONVERT_TO_DOUBLE(w, iw);
    ...

1
@Jean-FrançoisFabre 我相信这是由于常量折叠引起的。 - Dimitris Fasarakis Hilliard
2
我认为暗示存在转换并且它们在后续处理中没有被不同对待的说法“几乎肯定”有点牵强,没有来源支持。 - miradulo
1
@Mitch - 特别是在这段代码中,这两个操作的执行时间没有区别。只有在OP的循环中才会出现差异。这个答案过于草率了。 - TigerhawkT3
2
你为什么只看 float_pow,即使它在慢速情况下都无法运行? - user2357112
2
@TigerhawkT3:4**44**4.0会被常量折叠处理。这是一个完全独立的效果。 - user2357112
显示剩余5条评论

3
因为一个是正确的,另一个是近似的。
>>> 334453647687345435634784453567231654765 ** 4.0
1.2512490121794596e+154
>>> 334453647687345435634784453567231654765 ** 4
125124901217945966595797084130108863452053981325370920366144
719991392270482919860036990488994139314813986665699000071678
41534843695972182197917378267300625

我不知道为什么那个踩贴者会踩,但我是因为这个答案没有回答问题而踩的。仅仅因为某个东西是正确的,并不意味着它更快或更慢。一个比另一个慢是因为一个可以使用C类型,而另一个必须使用Python对象。 - Dimitris Fasarakis Hilliard
3
感谢您的解释。其实,我认为显而易见的是,仅计算一个数的大约12位左右的近似值比准确地计算所有数字要快得多。毕竟,我们使用近似值的唯一原因就是它们更快速地进行计算,对吗? - Veky

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接