为什么像 a *= b 这样的就地整数运算比 a = a * b 慢?

ada*_*kwm 14 python

我知道整数是不可变的,因此计算值不会修改原始整数。因此,就地操作应该与简单操作相同,1. 计算值和 2. 将值重新分配回变量。但是为什么就地操作比简单操作慢呢?

import timeit
print("a = a + 1: ", end="")
print(timeit.timeit("for i in range(100): a = a + 1", setup="a = 0"))
print("a += 1: ", end="")
print(timeit.timeit("for i in range(100): a += 1", setup="a = 0"))

print("a = a - 1: ", end="")
print(timeit.timeit("for i in range(100): a = a - 1", setup="a = 0"))
print("a -= 1: ", end="")
print(timeit.timeit("for i in range(100): a -= 1", setup="a = 0"))

print("a = a * 1: ", end="")
print(timeit.timeit("for i in range(100): a = a * 1", setup="a = 1"))
print("a *= 1: ", end="")
print(timeit.timeit("for i in range(100): a *= 1", setup="a = 1"))

print("a = a // 1: ", end="")
print(timeit.timeit("for i in range(100): a = a // 1", setup="a = 1"))
print("a //= 1: ", end="")
print(timeit.timeit("for i in range(100): a //= 1", setup="a = 1"))
Run Code Online (Sandbox Code Playgroud)

输出:

a = a + 1: 2.922127154
a += 1: 2.9701245480000003
a = a - 1: 2.9568866799999993
a -= 1: 3.1065419050000003
a = a * 1: 2.2483990140000003
a *= 1: 2.703524648
a = a // 1: 2.534561783000001
a //= 1: 2.6582312889999997
Run Code Online (Sandbox Code Playgroud)

所有就地操作都比简单操作慢。加法的差异最小,而乘法的差异最大。

Ray*_*ger 3

简短回答

就地操作的工作稍微多一些,因为它必须确定是否已定义自定义就地操作或是否回退到正常的二进制操作。

时间安排

对我来说,时间几乎相同:

$ python3.9 -m timeit -s 'a=1' 'a *= 1'
10000000 loops, best of 5: 27.6 nsec per loop

$ python3.9 -m timeit -s 'a=1' 'a = a * 1'
10000000 loops, best of 5: 27.8 nsec per loop
Run Code Online (Sandbox Code Playgroud)

解释

我预计就地版本会稍微慢一些,因为调度代码首先检查要定义的就地槽,然后回退到常规二进制操作。

需要一些时间才能确定尚未为intfloat等不可变对象定义就地槽。

也就是说,几乎所有其余代码都是相同的,这就是时间如此接近的原因。

深入了解源头

相关代码在Objects/abstract.c中:

/* The in-place operators are defined to fall back to the 'normal',
   non in-place operations, if the in-place methods are not in place.

   - If the left hand object has the appropriate struct members, and
     they are filled, call the appropriate function and return the
     result.  No coercion is done on the arguments; the left-hand object
     is the one the operation is performed on, and it's up to the
     function to deal with the right-hand object.

   - Otherwise, in-place modification is not supported. Handle it exactly as
     a non in-place operation of the same kind.

   */

static PyObject *
binary_iop1(PyObject *v, PyObject *w, const int iop_slot, const int op_slot
            )
{
    PyNumberMethods *mv = Py_TYPE(v)->tp_as_number;
    if (mv != NULL) {
        binaryfunc slot = NB_BINOP(mv, iop_slot);
        if (slot) {
            PyObject *x = (slot)(v, w);
            if (x != Py_NotImplemented) {
                return x;
            }
            Py_DECREF(x);
        }
    }
    return binary_op1(v, w, op_slot);
}
Run Code Online (Sandbox Code Playgroud)