Python内置的sum函数为什么比手动求和慢?

6

以下是我的简单代码示例:

import time

t0 = time.time()
s = 0
for i in range(1000000):
    s += i
t1 = time.time()

print(s, t1 - t0)

t0 = time.time()
s = sum(i for i in range(1000000))
t1 = time.time()

print(s, t1 - t0)

在我的电脑上(使用Python 3.8),它会输出:

499999500000 0.22901296615600586
499999500000 1.6930372714996338

所以,执行一百万次 += 比调用 sum 快 7 倍?这真的出乎意料。它在做什么?
编辑: 我愚蠢地允许调试器连接到进程并干扰我的测量,最终导致速度变慢。去掉调试器后,测量结果不再那么不可预测了。正如一些答案明确表明的那样,我观察到的情况不应该发生。

第二个版本是否可能构建整个列表以便迭代它? - vonludi
2
我正在使用Python 3.6.9,对我来说第二个版本比第一个版本快两倍。但是第二个版本有两个循环层级(从sum的隐式循环和for的显式循环)。消除不必要的for循环可以将速度提高三倍:sum(range(1000000)) - Tom Karzes
@vonludi:它不是。它是一个生成器表达式。但是,是的,那似乎导致了问题。由于某种原因,生成器非常缓慢。 - zvone
1
sum(range(1000000)) 更快:您不需要使用生成器表达式迭代 range 对象,以便 sum 具有可迭代对象。 - chepner
1
使用Python 3.7或Python 3.8中的任一版本,生成器版本的速度是循环版本的两倍,而sum(range(...))比生成器版本又快了两倍。 - chepner
显示剩余2条评论
4个回答

5

让我们使用 timeit 进行适当的基准测试,并且为了轻松比较不同的 Python 版本,让我们在 Docker 容器中运行:

so62514160.py

N = 1000000

def m1():
    s = 0
    for i in range(N):
        s += i

def m2():
    s = sum(i for i in range(N))

def m3():
    s = sum(range(N))

so62514160bench.sh

for image in python:2.7 python:3.6 python:3.7 python:3.8; do
    for fun in m1 m2 m3; do
        echo -n "$image" "$fun "
        docker run --rm -it -v $(pwd):/app -w /app -e PYTHONDONTWRITEBYTECODE=1 "$image" python -m timeit -s 'import so62514160 as s' "s.$fun()"
    done
done

我的机器上的结果:

python:2.7 m1 10 loops, best of 3:  43.5 msec per loop
python:2.7 m2 10 loops, best of 3:  39.6 msec per loop
python:2.7 m3 100 loops, best of 3: 17.1 msec per loop
python:3.6 m1 10 loops, best of 3:  41.9 msec per loop
python:3.6 m2 10 loops, best of 3:  46 msec per loop
python:3.6 m3 100 loops, best of 3: 17.7 msec per loop
python:3.7 m1 5 loops, best of 5:   45 msec per loop
python:3.7 m2 5 loops, best of 5:   40.7 msec per loop
python:3.7 m3 20 loops, best of 5:  17.3 msec per loop
python:3.8 m1 5 loops, best of 5:   48.2 msec per loop
python:3.8 m2 5 loops, best of 5:   44.6 msec per loop
python:3.8 m3 10 loops, best of 5:  19.2 msec per loop

绘图

输入图片描述


演示如何进行正确的基准测试的好方法! - jpnadas
为什么要使用“最佳”而不是平均值,并使用不同数量的循环? - OverLordGoldDragon
这是否表明Python 3.8比以前的版本稍微慢一些?还是这些差异太小而不足为重? - debsim
这篇文章并没有回答为什么m3更快的问题,只是展示了更多的基准测试数据,所以它获得的赞并没有解释为什么m3更快。 - OverLordGoldDragon
@norok2 这些信息既不在你的回答中,也不在这个回答中。问题是为什么会发生,而不是它是否发生。诚然,原始问题没有包括 sum(range()),但所有的答案仍然都是关于“这就是它是什么”的,而不是“这就是它为什么”的。 - OverLordGoldDragon
显示剩余5条评论

2
首先,你的观察结果可能不适用于其他系统,因为你的测量方式非常不可靠,容易受到性能波动的影响,这些性能波动很可能由操作系统对测量时系统负载的反应所主导。你应该使用timeit或类似的工具。
例如,在Python 3.6虚拟环境(Google Colab)中,我得到了以下时间(这似乎在其他答案中也是相当可重现的):
import numba as nb


def sum_loop(n):
    result = 0
    for x in range(n):
        result += x
    return result


sum_loop_nb = nb.jit(sum_loop)
sum_loop_nb.__name__ = 'sum_loop_nb'


def sum_analytical(n):
    return n * (n - 1) // 2


def sum_list(n):
    return sum([x for x in range(n)])


def sum_gen(n):
    return sum(x for x in range(n))


def sum_range(n):
    return sum(range(n))


sum_loop_nb(10)  # to trigger compilation


funcs = sum_analytical, sum_loop, sum_loop_nb, sum_gen, sum_list, sum_range


n = 1000000
for func in funcs:
    print(func.__name__, func(n))
    %timeit func(n)
# sum_analytical 499999500000
# 10000000 loops, best of 3: 222 ns per loop
# sum_loop 499999500000
# 10 loops, best of 3: 55.6 ms per loop
# sum_loop_nb 499999500000
# 10000000 loops, best of 3: 196 ns per loop
# sum_gen 499999500000
# 10 loops, best of 3: 51.7 ms per loop
# sum_list 499999500000
# 10 loops, best of 3: 68.4 ms per loop
# sum_range 499999500000
# 100 loops, best of 3: 17.8 ms per loop

这些Python版本的计时差异不大。

sum_analytical()sum_loop_nb()版本只是为了好玩而已,没有进一步分析。 sum_list()与其他版本的表现有很大不同,因为它为计算创建了一个大的、基本上不必要的对象,也没有进一步分析。


当然,导致这些不同计时的原因在于所考虑的函数版本产生的字节码。特别是从sum_loop()sum_range(),代码逐渐变得更简单:

import dis

funcs = sum_loop, sum_gen, sum_range
for func in funcs:
    print(func.__name__)
    print(dis.dis(func))
    print()

# sum_loop
#   2           0 LOAD_CONST               1 (0)
#               2 STORE_FAST               1 (result)

#   3           4 SETUP_LOOP              24 (to 30)
#               6 LOAD_GLOBAL              0 (range)
#               8 LOAD_FAST                0 (n)
#              10 CALL_FUNCTION            1
#              12 GET_ITER
#         >>   14 FOR_ITER                12 (to 28)
#              16 STORE_FAST               2 (x)

#   4          18 LOAD_FAST                1 (result)
#              20 LOAD_FAST                2 (x)
#              22 INPLACE_ADD
#              24 STORE_FAST               1 (result)
#              26 JUMP_ABSOLUTE           14
#         >>   28 POP_BLOCK

#   5     >>   30 LOAD_FAST                1 (result)
#              32 RETURN_VALUE
# None

# sum_gen
#   9           0 LOAD_GLOBAL              0 (sum)
#               2 LOAD_CONST               1 (<code object <genexpr> at 0x7f86d67c49c0, file "<ipython-input-4-9519b0039c88>", line 9>)
#               4 LOAD_CONST               2 ('sum_gen.<locals>.<genexpr>')
#               6 MAKE_FUNCTION            0
#               8 LOAD_GLOBAL              1 (range)
#              10 LOAD_FAST                0 (n)
#              12 CALL_FUNCTION            1
#              14 GET_ITER
#              16 CALL_FUNCTION            1
#              18 CALL_FUNCTION            1
#              20 RETURN_VALUE
# None

# sum_range
#  13           0 LOAD_GLOBAL              0 (sum)
#               2 LOAD_GLOBAL              1 (range)
#               4 LOAD_FAST                0 (n)
#               6 CALL_FUNCTION            1
#               8 CALL_FUNCTION            1
#              10 RETURN_VALUE
# None

除非其他人提供更好的“解释”(而不是更多的基准测试),否则应接受此答案;我还会指向评论,其中包括一些进一步的信息。 - OverLordGoldDragon

1

啊,我自己找到了答案,但是它又引出了另一个问题。

所以,这样会更快:

t0 = time.time()
s = sum(range(1000000))
t1 = time.time()

print(s, t1 - t0)

结果是:

这里填写结果

499999500000 0.05099987983703613

所以,如预期的那样,sum+= 更快,但生成器表达式 (i for i in range(n)) 比任何其他方法都要慢得多。我必须说这也相当令人惊讶。

可能是由于sum在生成器上的实现导致的。你尝试使用列表而不是生成器来看看它产生了什么结果吗?sum([i for i in range(n)]) - jpnadas
3
顺便说一下,这不是我们基准测试代码的方式。请查看 timeit 模块。 - Azat Ibrakov
1
另外,如果我没记错的话,range(n)已经给你一个生成器了...所以你正在遍历一个生成器来重新创建它。似乎有点浪费。也许sum(range(n))是最快的,因为sum的低级实现。 - jpnadas
1
@jpnadas 是的,但这只是我实际代码中简化的版本,在此之前我对i执行了一些操作。顺便说一句,sum([i for i in range(1000000)])sum(i for i in range(1000000))更快! - zvone
很好,@zvone...那么在实际实现中,哪个更快,sum([operations(i) for i in my_list])还是+=的解决方案? - jpnadas
显示剩余4条评论

0

我得到了不同的数字

python3 main.py
499999500000 0.0832064151763916
499999500000 0.03934478759765625

从这段代码中

import time

to0 = []
to1 = []

for i in range(1000):
    t0 = time.time()
    s = 0
    for i in range(1000000):
        s += i
    t1 = time.time()

    to0.append(t1 - t0)

    t0 = time.time()
    s = sum(i for i in range(1000000))
    t1 = time.time()

    to1.append(t1 - t0)


print(sum(to0)/len(to0))
print(sum(to1)/len(to1))

我得到了

0.07862246823310852
0.0318267240524292

尝试更新Python,这个程序运行在Python 3.7.3上。


1
嗯,问题是我的Python比你的新!也许这是在3.8中引入的一个bug。 - zvone
1
使用Python 3.8.2,我得到了0.09662985801696777和0.03805732727050781。 - user13783164

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