【问题标题】:Why is x**4.0 faster than x**4 in Python 3?为什么在 Python 3 中 x**4.0 比 x**4 快?
【发布时间】:2017-07-10 08:38:17
【问题描述】:

为什么x**4.0x**4 快?我正在使用 CPython 3.5.2。

$ 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 指出它不会发生在循环之外。我检查了一下,这种情况只发生在 base 被提升时(据我所见)。有什么想法吗?

【问题讨论】:

  • 对于它的价值:Python 2.7.13 对我来说快了 2~3 倍,and 显示了相反的行为:整数指数比浮点指数快.
  • @Evert 是的,x**4.0 获得了 14 微秒,x**4 获得了 3.9 微秒。

标签: python performance python-3.x python-3.5 python-internals


【解决方案1】:

因为一个是正确的,另一个是近似值。

>>> 334453647687345435634784453567231654765 ** 4.0
1.2512490121794596e+154
>>> 334453647687345435634784453567231654765 ** 4
125124901217945966595797084130108863452053981325370920366144
719991392270482919860036990488994139314813986665699000071678
41534843695972182197917378267300625

【讨论】:

  • 我不知道为什么那个投反对票的人投了反对票,但我这样做了,因为这个答案没有回答这个问题。仅仅因为某件事是正确的,并不意味着它更快或更慢。一个比另一个慢,因为一个可以处理 C 类型,而另一个必须处理 Python 对象。
  • 感谢您的解释。好吧,我真的认为很明显,只计算一个数字到 12 位左右的数字的近似值比精确计算所有数字要快。毕竟,我们使用近似值的唯一原因是它们的计算速度更快,对吧?
【解决方案2】:

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

Python 3 int 对象是一个成熟的对象,旨在支持任意大小;由于这个事实,它们是handled as such on the C level(请参阅long_pow 中的所有变量如何声明为PyLongObject * 类型)。这也使得它们的求幂变得更加更复杂乏味,因为您需要使用 ob_digit 数组来表示其执行它的值。 (Source for the brave. -- 有关PyLongObjects 的更多信息,请参阅:Understanding memory allocation for large integers in Python。)

Python float 对象,相反,可以转换为 C double 类型(通过使用PyFloat_AsDouble)并且可以执行操作using those native types这太棒了,因为在检查了相关的边缘情况后,它允许 Python use the platforms' pow (C's pow, that is) 处理实际的求幂:

/* 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 是我们原来的PyFloatObjects 作为C doubles。

对于它的价值: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'>

你在这里看到的&lt;type 'int'&gt;做同样的事情floats 做,它被安全地转换为 C longwhen exponentiation is performed on itint_pow 还提示编译器如果可以的话,把它们放在一个寄存器中,这样可以有所作为):

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

这样可以获得良好的速度增益。

要查看&lt;type 'long'&gt;s 与&lt;type 'int'&gt;s 相比有多迟钝,如果您在 Python 2 中将 x 名称包装在 long 调用中(本质上是强制它使用 long_pow,就像在 Python 3 中一样),速度增益消失:

# <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

请注意,虽然一个 sn-p 将 int 转换为 long 而另一个没有(正如 @pydsinger 所指出的那样),但这个演员并不是导致减速的原因。 long_pow 的实现是。 (仅使用long(x) 对语句计时以查看)。

[...] 它不会发生在循环之外。 [...] 对此有什么想法吗?

这是 CPython 的窥孔优化器为您折叠常量。无论哪种情况,您都会得到相同的确切时间,因为没有实际计算来找到求幂的结果,只加载值:

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 加载float 256.0 而不是int 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

所以时间是相同的。


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

【讨论】:

  • 不管它是什么,它都与range 上的循环有关,因为仅计时** 操作本身不会产生整数和浮点数之间的差异。
  • 只有在查找变量时才会出现差异(4**44**4.0 一样快),而这个答案根本没有涉及到这一点。
  • 但是,常量将被折叠@TigerhawkT3 (dis(compile('4 ** 4', '', 'exec'))),所以时间应该完全相同相同。
  • 您最后的时间似乎没有显示您所说的内容。 long(x)**2. 仍然比 long(x)**2 快 4-5 倍。 (不过,不是反对者之一)
  • @mbomb007 在 Python 3 中消除了 &lt;type 'long'&gt; 类型可能是为了简化语言所做的努力。如果您可以使用一种类型来表示整数,那么它比两种更易于管理(并且担心在必要时从一种类型转换为另一种类型,用户会感到困惑等)。速度增益是次要的。 PEP 237 的基本原理部分也提供了更多见解。
【解决方案3】:

如果我们查看字节码,我们可以看到表达式完全相同。唯一的区别是常量类型将是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,这里太长粘贴不上,所以here's the link

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

最后,在float_pow()Objects/floatobject.c 的第 686 行,我们看到参数在实际操作之前被转换为 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);
    ...

【讨论】:

  • @Jean-FrançoisFabre 我相信这是由于不断折叠。
  • 我认为暗示存在转换并且在“最肯定”的情况下没有以不同方式处理它们的含义在没有来源的情况下有点牵强。
  • @Mitch - 特别是因为在这个特定的代码中,这两个操作的执行时间没有区别。差异仅出现在 OP 的循环中。这个答案是草率的结论。
  • 为什么你只看float_pow,而对于慢速的情况甚至都不适用?
  • @TigerhawkT3: 4**44**4.0 得到常量折叠。这是一个完全独立的效果。
猜你喜欢
  • 2019-05-03
  • 2015-05-07
  • 2013-08-29
  • 2019-08-02
  • 2020-06-26
  • 2012-04-06
  • 2016-09-26
  • 2011-12-14
  • 2021-03-11
相关资源
最近更新 更多