Python内置的sum函数与for循环性能比较

20

我注意到Python内置的sum函数在对100万个整数的列表求和时,大约比使用for循环快3倍:

import timeit

def sum1():
    s = 0
    for i in range(1000000):
        s += i
    return s

def sum2():
    return sum(range(1000000))

print 'For Loop Sum:', timeit.timeit(sum1, number=10)
print 'Built-in Sum:', timeit.timeit(sum2, number=10)

# Prints:
# For Loop Sum: 0.751425027847
# Built-in Sum: 0.266746997833

为什么这样?sum是如何实现的?


8
sum 函数是在 Python 解释器内部用 C 语言实现的,而你的 for 循环需要被解释执行,所以它运行较慢是正常的。 - Matteo Italia
CPython 中,内置函数比纯 Python 翻译要快得多。这就是为什么优化 CPython 的一个好方法是让内置函数尽可能多地完成工作。请注意,使用其他实现(如 PyPy)会完全改变这种情况。 - Bakuriu
使用numpy怎么样?当然,你需要先创建数组,所以对于一次性使用来说,速度可能会慢一些;但如果你已经有了数组,我认为arr.sum()更快。 - dwanderson
@dwanderson 我不知道即使只使用一次,它是否会变慢。从数字中获取值非常容易和高效,因此创建数组可能比求和更快(这还需要执行加法)。然后计算总和应该需要*更少的时间,因此它可能更快。但是 NumPy 有一个大问题:它使用固定大小整数,因此对于长数组,它很容易溢出或者你必须使用 object 数组,这会大大降低性能。 - Bakuriu
@Bakuriu:请看我的答案。至少对于大数据来说,它要快得多。 - DrV
显示剩余3条评论
4个回答

29

实际上,速度差异超过3倍,但您可以通过首先创建一个包含100万个整数的大型内存列表来减慢任一版本。将其从时间试验中分离出来:

>>> import timeit
>>> def sum1(lst):
...     s = 0
...     for i in lst:
...         s += i
...     return s
... 
>>> def sum2(lst):
...     return sum(lst)
... 
>>> values = range(1000000)
>>> timeit.timeit('f(lst)', 'from __main__ import sum1 as f, values as lst', number=100)
3.457869052886963
>>> timeit.timeit('f(lst)', 'from __main__ import sum2 as f, values as lst', number=100)
0.6696369647979736

速度差异现在已经增加到超过5倍。

for循环是解释执行Python字节码的。 sum()完全在C代码中循环。 解释执行的字节码和C代码之间的速度差异很大。

此外,C代码确保如果可以将总和保留在C类型中,则不会创建新的Python对象; 这适用于intfloat结果。

反汇编的Python版本执行以下操作:

>>> import dis
>>> def sum1():
...     s = 0
...     for i in range(1000000):
...         s += i
...     return s
... 
>>> dis.dis(sum1)
  2           0 LOAD_CONST               1 (0)
              3 STORE_FAST               0 (s)

  3           6 SETUP_LOOP              30 (to 39)
              9 LOAD_GLOBAL              0 (range)
             12 LOAD_CONST               2 (1000000)
             15 CALL_FUNCTION            1
             18 GET_ITER            
        >>   19 FOR_ITER                16 (to 38)
             22 STORE_FAST               1 (i)

  4          25 LOAD_FAST                0 (s)
             28 LOAD_FAST                1 (i)
             31 INPLACE_ADD         
             32 STORE_FAST               0 (s)
             35 JUMP_ABSOLUTE           19
        >>   38 POP_BLOCK           

  5     >>   39 LOAD_FAST                0 (s)
             42 RETURN_VALUE        

除了解释器循环比C慢之外,INPLACE_ADD 还会创建一个新的整数对象(超过255,CPython将小的int对象作为单例缓存)。

你可以在Python 水银代码仓库中查看C实现,但它在评论中明确说明:

/* Fast addition by keeping temporary sums in C instead of new Python objects.
   Assumes all inputs are the same type.  If the assumption fails, default
   to the more general routine.
*/

+1 提到单例模式。当我开始编程时,我犯了一个错误,使用 is 检查数字相等性,有时候它能正常工作,让我感到惊讶。现在,我发现例如 65536 is 65536 返回 True1<<16 is 1<<16 返回 False,而 1<<8 is 1<<8 返回 True,所以我猜它只适用于256?并且特殊处理硬编码的数字?现在我更加困惑了... - dwanderson
@user2357112:检查源代码;整数情况下的循环从long i_result = PyInt_AS_LONG(result)开始。一旦溢出,它就会切换回使用Python的long对象。 - Martijn Pieters
@dwanderson:Python在编译的字节码中还会为代码中使用的任何不可变文字值创建常量(即使是在交互式解释器中)。这也适用于许多计算出的常量,但不适用于<<位移。 - Martijn Pieters
@dwanderson:在这个答案的反汇编中,你也可以看到这一点;LOAD_CONST用于循环的1000000值。 - Martijn Pieters
@user2357112:不,sum(range(1000000)) 完全适合我的 sys.maxint 值。 - Martijn Pieters
显示剩余4条评论

5

正如 dwanderson 建议的那样,Numpy 是一个替代方案。如果你想进行一些数学计算,确实是一个不错的选择。请参考这个基准测试:

import numpy as np

r = range(1000000)       # 12.5 ms
s = sum(r)               # 7.9 ms

ar = np.arange(1000000)  # 0.5 ms
as = np.sum(ar)          # 0.6 ms

因此,使用numpy创建列表和计算总和速度更快。 这主要是因为numpy.array专为此而设计,比列表更加高效。
然而,如果我们有一个Python列表,则numpy非常慢,因为其从列表转换为numpy.array的过程很缓慢。
r = range(1000000)
ar = np.array(r)         # 102 ms

3

然而,如果循环只是从0开始每次加1,那么可以使用快速技巧加法。对于range(1000000),总和输出应为499999500000。

import timeit

def sum1():
    s = 0
    for i in range(1000000):
        s += i
    #print s    
    return s

def sum2():

    return sum(range(1000000))

def sum3():
    s = range(1000000)
    s = ((s[1]+s[-1])/2) * (len(s)-1)
    #print(s)
    return s

print 'For Loop Sum:', timeit.timeit(sum1, number=10)
print 'Built-in Sum:', timeit.timeit(sum2, number=10)
print 'Fast Sum:', timeit.timeit(sum3, number=10)

#prints
#For Loop Sum: 1.8420711
#Built-in Sum: 1.1081646
#Fast Sum: 0.3191561

sum3循环在这种情况下非常有用,因为它可以在O(1)时间内计算总和。但它只能用于某些问题。 - Muhammad Khuzaima Umair

1
你可以在Python/bltinmodule.c中查看源代码。它有针对intfloat的特殊情况,但由于总和很快就会溢出到long,因此这可能对性能没有重大影响。一般情况下的逻辑与你在Python中编写的逻辑非常相似,只是用C语言编写。加速最可能是因为它不必经过所有字节码解释和错误处理开销。
static PyObject*
builtin_sum(PyObject *self, PyObject *args)
{
    PyObject *seq;
    PyObject *result = NULL;
    PyObject *temp, *item, *iter;

    if (!PyArg_UnpackTuple(args, "sum", 1, 2, &seq, &result))
        return NULL;

    iter = PyObject_GetIter(seq);
    if (iter == NULL)
        return NULL;

    if (result == NULL) {
        result = PyInt_FromLong(0);
        if (result == NULL) {
            Py_DECREF(iter);
            return NULL;
        }
    } else {
        /* reject string values for 'start' parameter */
        if (PyObject_TypeCheck(result, &PyBaseString_Type)) {
            PyErr_SetString(PyExc_TypeError,
                "sum() can't sum strings [use ''.join(seq) instead]");
            Py_DECREF(iter);
            return NULL;
        }
        Py_INCREF(result);
    }

#ifndef SLOW_SUM
    /* Fast addition by keeping temporary sums in C instead of new Python objects.
       Assumes all inputs are the same type.  If the assumption fails, default
       to the more general routine.
    */
    if (PyInt_CheckExact(result)) {
        long i_result = PyInt_AS_LONG(result);
        Py_DECREF(result);
        result = NULL;
        while(result == NULL) {
            item = PyIter_Next(iter);
            if (item == NULL) {
                Py_DECREF(iter);
                if (PyErr_Occurred())
                    return NULL;
                return PyInt_FromLong(i_result);
            }
            if (PyInt_CheckExact(item)) {
                long b = PyInt_AS_LONG(item);
                long x = i_result + b;
                if ((x^i_result) >= 0 || (x^b) >= 0) {
                    i_result = x;
                    Py_DECREF(item);
                    continue;
                }
            }
            /* Either overflowed or is not an int. Restore real objects and process normally */
            result = PyInt_FromLong(i_result);
            temp = PyNumber_Add(result, item);
            Py_DECREF(result);
            Py_DECREF(item);
            result = temp;
            if (result == NULL) {
                Py_DECREF(iter);
                return NULL;
            }
        }
    }

    if (PyFloat_CheckExact(result)) {
        double f_result = PyFloat_AS_DOUBLE(result);
        Py_DECREF(result);
        result = NULL;
        while(result == NULL) {
            item = PyIter_Next(iter);
            if (item == NULL) {
                Py_DECREF(iter);
                if (PyErr_Occurred())
                    return NULL;
                return PyFloat_FromDouble(f_result);
            }
            if (PyFloat_CheckExact(item)) {
                PyFPE_START_PROTECT("add", Py_DECREF(item); Py_DECREF(iter); return 0)
                f_result += PyFloat_AS_DOUBLE(item);
                PyFPE_END_PROTECT(f_result)
                Py_DECREF(item);
                continue;
            }
            if (PyInt_CheckExact(item)) {
                PyFPE_START_PROTECT("add", Py_DECREF(item); Py_DECREF(iter); return 0)
                f_result += (double)PyInt_AS_LONG(item);
                PyFPE_END_PROTECT(f_result)
                Py_DECREF(item);
                continue;
            }
            result = PyFloat_FromDouble(f_result);
            temp = PyNumber_Add(result, item);
            Py_DECREF(result);
            Py_DECREF(item);
            result = temp;
            if (result == NULL) {
                Py_DECREF(iter);
                return NULL;
            }
        }
    }
#endif

    for(;;) {
        item = PyIter_Next(iter);
        if (item == NULL) {
            /* error, or end-of-sequence */
            if (PyErr_Occurred()) {
                Py_DECREF(result);
                result = NULL;
            }
            break;
        }
        /* It's tempting to use PyNumber_InPlaceAdd instead of
           PyNumber_Add here, to avoid quadratic running time
           when doing 'sum(list_of_lists, [])'.  However, this
           would produce a change in behaviour: a snippet like

             empty = []
             sum([[x] for x in range(10)], empty)

           would change the value of empty. */
        temp = PyNumber_Add(result, item);
        Py_DECREF(result);
        Py_DECREF(item);
        result = temp;
        if (result == NULL)
            break;
    }
    Py_DECREF(iter);
    return result;
}

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