You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为什么`np.sum(range(N))`运行极慢?Python与Numpy求和性能差异探究

问题描述

我之前看过一个讲解Python循环运行速度的视频,里面提到sum(range(N))比手动遍历range逐次累加的速度快得多,原因是前者调用内置函数在C层运行,而后者的求和操作是在运行效率更低的Python层完成的。我好奇引入numpy后的求和性能表现,测试后发现符合预期的np.sum(np.arange(N))是最快的,但sum(np.arange(N))和np.sum(range(N))的运行速度甚至比朴素for循环还慢。

这种情况出现的原因是什么?

以下是我使用的测试脚本,包含我已知的性能变慢原因注释(大多来自上述视频),以及我在python 3.10.0、numpy 1.21.2环境下得到的运行结果:

更新后的脚本:

import numpy as np
from timeit import timeit

N = 10_000_000
repetition = 10

def sum0(N = N):
    s = 0
    i = 0
    while i < N: # 条件判断在Python层执行
        s += i
        i += 1 # 两次加法操作都在Python层执行
    return s

def sum1(N = N):
    s = 0
    for i in range(N): # 自增操作在C层执行
        s += i # 加法在Python层执行
    return s

def sum2(N = N):
    return sum(range(N)) # 所有操作都在C层执行

def sum3(N = N):
    return sum(list(range(N)))

def sum4(N = N):
    return np.sum(range(N)) # 转np.array的过程非常慢

def sum5(N = N):
    # 转np.array的过程快很多
    return np.sum(np.fromiter(range(N),dtype = int))

def sum5v2_(N = N):
    # 转np.array的过程快很多
    return np.sum(np.fromiter(range(N),dtype = np.int_))

def sum6(N = N):
    # 从np.int转Py_long的过程可能很慢
    return sum(np.arange(N))

def sum7(N = N):
    # list返回由np.int组成的列表
    return sum(list(np.arange(N)))

def sum7v2(N = N):
    # 转python int的操作,用tolist似乎比sum(list())里的隐式转换更快
    # tolist返回由python int组成的列表
    return sum(np.arange(N).tolist())

def sum8(N = N):
    return np.sum(np.arange(N)) # 所有操作都在numpy层执行(底层是fortran的libblas?)

def sum9(N = N):
    return np.arange(N).sum() # 省去了分发开销

def array_basic(N = N):
    return np.array(range(N))

def array_dtype(N = N):
    return np.array(range(N),dtype = np.int_)

def array_iter(N = N):
    # np.sum的源码提到可以用fromiter来转换生成器
    return np.fromiter(range(N),dtype = np.int_)

print(f"while loop:         {timeit(sum0, number = repetition)}")
print(f"for loop:           {timeit(sum1, number = repetition)}")
print(f"sum_range:          {timeit(sum2, number = repetition)}")
print(f"sum_rangelist:      {timeit(sum3, number = repetition)}")
print(f"npsum_range:        {timeit(sum4, number = repetition)}")
print(f"npsum_iterrange:    {timeit(sum5, number = repetition)}")
print(f"npsum_iterrangev2:  {timeit(sum5, number = repetition)}")
print(f"sum_arange:         {timeit(sum6, number = repetition)}")
print(f"sum_list_arange:    {timeit(sum7, number = repetition)}")
print(f"sum_arange_tolist:  {timeit(sum7v2, number = repetition)}")
print(f"npsum_arange:       {timeit(sum8, number = repetition)}")
print(f"nparangenpsum:      {timeit(sum9, number = repetition)}")
print(f"array_basic:        {timeit(array_basic, number = repetition)}")
print(f"array_dtype:        {timeit(array_dtype, number = repetition)}")
print(f"array_iter:         {timeit(array_iter,  number = repetition)}")

print(f"npsumarangeREP:     {timeit(lambda : sum8(N/1000), number = 100000*repetition)}")
print(f"npsumarangeREP:     {timeit(lambda : sum9(N/1000), number = 100000*repetition)}")

# 示例输出:
#
# while loop:         11.493371912998555
# for loop:           7.385945574002108
# sum_range:          2.4605720699983067
# sum_rangelist:      4.509678105998319
# npsum_range:        11.85120212900074
# npsum_iterrange:    4.464334709002287
# npsum_iterrangev2:  4.498494338993623
# sum_arange:         9.537815956995473
# sum_list_arange:    13.290120724996086
# sum_arange_tolist:  5.231948580003518
# npsum_arange:       0.241889145996538
# nparangenpsum:      0.21876695199898677
# array_basic:        11.736577274998126
# array_dtype:        8.71628468400013
# array_iter:         4.303306431000237
# npsumarangeREP:     21.240833958996518
# npsumarangeREP:     16.690092379001726

原因解答

两种慢的场景核心都是跨类型转换的开销大于底层优化带来的收益,具体拆解如下:

  1. np.sum(range(N))慢的原因
    np.sum是为numpy数组设计的优化函数,当输入是Python原生的可迭代对象(比如range)时,它会先把整个可迭代对象逐元素转换成numpy数组。默认的np.array(range(N))转换过程需要逐个处理Python int对象,每次都要做类型校验、内存申请,开销远大于纯Python循环的累加,所以整体耗时会超过朴素for循环。你测试里用np.fromiter转换的sum5比sum4快3倍左右,就是因为fromiter是专门为Python生成器/可迭代对象设计的数组转换接口,预分配了内存,减少了中间开销。

  2. sum(np.arange(N))慢的原因
    Python内置的sum是为Python原生对象设计的,当输入是numpy数组时,它会逐元素遍历数组,每次取出的元素是numpy的int类型,累加前必须先转换成Python原生的int类型,这个类型转换每一步都有Python层的开销,相当于比纯Python for循环多了一步类型转换的成本,所以速度反而更慢。你测试里的sum7v2用tolist()先把整个numpy数组批量转成Python int的列表再求和,速度比sum6快近一倍,就是因为批量转换的开销远低于逐元素隐式转换的开销。

而你测试里最快的np.sum(np.arange(N)),整个流程的数组生成、求和操作都在numpy的C层完成,没有Python层的循环和类型转换开销,底层还用到了向量化、BLAS库优化,所以速度会比纯C实现的sum(range(N))还要快一个数量级。


内容的提问来源于stack exchange,提问作者fbence

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.30 12:39:03