【问题标题】:Python's sum vs. NumPy's numpy.sumPython sum 与 NumPy numpy.sum
【发布时间】:2012-06-10 22:36:26
【问题描述】:

使用 Python 的原生 sum 函数和 NumPy 的 numpy.sum 在性能和行为上有何不同? sum 适用于 NumPy 的数组,numpy.sum 适用于 Python 列表,它们都返回相同的有效结果(尚未测试溢出等边缘情况)但类型不同。

>>> import numpy as np
>>> np_a = np.array(range(5))
>>> np_a
array([0, 1, 2, 3, 4])
>>> type(np_a)
<class 'numpy.ndarray')

>>> py_a = list(range(5))
>>> py_a
[0, 1, 2, 3, 4]
>>> type(py_a)
<class 'list'>

# The numerical answer (10) is the same for the following sums:
>>> type(np.sum(np_a))
<class 'numpy.int32'>
>>> type(sum(np_a))
<class 'numpy.int32'>
>>> type(np.sum(py_a))
<class 'numpy.int32'>
>>> type(sum(py_a))
<class 'int'>

编辑:我认为我的实际问题是在 Python 整数列表上使用 numpy.sum 会比使用 Python 自己的 sum 更快吗?

此外,使用 Python 整数与标量 numpy.int32 有何影响(包括性能)?例如,对于a += 1,如果a 的类型是Python 整数或numpy.int32,是否存在行为或性能差异?我很好奇在 Python 代码中使用诸如 numpy.int32 之类的 NumPy 标量数据类型是否更快。

为了澄清,我正在进行生物信息学模拟,其中部分包括将多维numpy.ndarrays 折叠成单个标量和,然后对其进行额外处理。我正在使用 Python 3.2 和 NumPy 1.6。

提前致谢!

【问题讨论】:

  • 这个问题的一个有趣方面是 Numba 如何影响比较...它在技术上支持 numpy,前提是 numpy 数组遵守 Numba 的当前规则 numpy 集成。

标签: python performance numpy python-3.x


【解决方案1】:

我很好奇并计时了。 numpy.sum 对于 numpy 数组似乎要快得多,但在列表上要慢得多。

import numpy as np
import timeit

x = range(1000)
# or 
#x = np.random.standard_normal(1000)

def pure_sum():
    return sum(x)

def numpy_sum():
    return np.sum(x)

n = 10000

t1 = timeit.timeit(pure_sum, number = n)
print 'Pure Python Sum:', t1
t2 = timeit.timeit(numpy_sum, number = n)
print 'Numpy Sum:', t2

x = range(1000)时的结果:

Pure Python Sum: 0.445913167735
Numpy Sum: 8.54926219673

x = np.random.standard_normal(1000)时的结果:

Pure Python Sum: 12.1442425643
Numpy Sum: 0.303303771848

我正在使用 Python 2.7.2 和 Numpy 1.6.1

【讨论】:

  • 你说得对,np.sum 在使用 np.array 时会更快。但是如果你计算np.sum(np.array object)sum(list object),两者的表现几乎一样。
【解决方案2】:

[...] 我的 [...] 问题是在 Python 整数列表中使用 numpy.sum 会比使用 Python 自己的 sum 更快吗?

这个问题的答案是:没有。

Pythons sum 在列表上会更快,而 NumPys sum 在数组上会更快。我实际上做了一个基准测试来显示时间(Python 3.6,NumPy 1.14):

import random
import numpy as np
import matplotlib.pyplot as plt

from simple_benchmark import benchmark

%matplotlib notebook

def numpy_sum(it):
    return np.sum(it)

def python_sum(it):
    return sum(it)

def numpy_sum_method(arr):
    return arr.sum()

b_array = benchmark(
    [numpy_sum, numpy_sum_method, python_sum],
    arguments={2**i: np.random.randint(0, 10, 2**i) for i in range(2, 21)},
    argument_name='array size',
    function_aliases={numpy_sum: 'numpy.sum(<array>)', numpy_sum_method: '<array>.sum()', python_sum: "sum(<array>)"}
)

b_list = benchmark(
    [numpy_sum, python_sum],
    arguments={2**i: [random.randint(0, 10) for _ in range(2**i)] for i in range(2, 21)},
    argument_name='list size',
    function_aliases={numpy_sum: 'numpy.sum(<list>)', python_sum: "sum(<list>)"}
)

有了这些结果:

f, (ax1, ax2) = plt.subplots(1, 2, sharey=True)
b_array.plot(ax=ax1)
b_list.plot(ax=ax2)

左:在 NumPy 数组上;右:在 Python 列表中。 请注意,这是一个对数图,因为基准测试涵盖了非常广泛的值。但是对于定性结果:越低越好。

这表明对于列表,Pythons sum 总是更快,而 np.sum 或数组上的 sum 方法会更快(除了非常短的数组,其中 Pythons sum 更快)。

以防你有兴趣将它们相互比较,我还制作了一个包含所有这些的情节:

f, ax = plt.subplots(1)
b_array.plot(ax=ax)
b_list.plot(ax=ax)
ax.grid(which='both')

有趣的是,numpy 可以在数组上与 Python 和列表竞争的点大约是 200 个元素!请注意,这个数字可能取决于很多因素,例如 Python/NumPy 版本,...不要太字面意思。

没有提到的是这种差异的原因(我的意思是大规模差异,而不是短列表/数组的差异,其中函数只是具有不同的常量开销)。假设 CPython,Python 列表是 C(语言 C)指向 Python 对象(在本例中为 Python 整数)的指针数组的包装器。这些整数可以看作是 C 整数的包装器(实际上并不正确,因为 Python 整数可以任意大,因此它不能简单地使用 one C 整数,但它足够接近)。

例如,像[1, 2, 3] 这样的列表将(在示意图上,我省略了一些细节)这样存储:

然而,NumPy 数组是包含 C 值的 C 数组的包装器(在本例中为 intlong,具体取决于 32 位或 64 位并取决于操作系统)。

所以像np.array([1, 2, 3]) 这样的 NumPy 数组看起来像这样:

接下来要了解的是这些函数是如何工作的:

  • Pythons sum 迭代可迭代对象(在本例中为列表或数组)并添加所有元素。
  • NumPys sum method 遍历存储的 C 数组并添加这些 C 值,最后将该值包装在 Python 类型中(在本例中为 numpy.int32(或 numpy.int64)并返回它。
  • NumPys sum 函数 将输入转换为 array(至少如果它不是数组的话),然后使用 NumPy sum 方法.

显然,从 C 数组中添加 C 值比添加 Python 对象要快得多,这就是 NumPy 函数可以快得多的原因(参见上面的第二张图,数组上的 NumPy 函数优于到目前为止,对于大型数组,Python 总和)。

但是将 Python 列表转换为 NumPy 数组相对较慢,然后您仍然需要添加 C 值。这就是为什么对于 lists,Python sum 会更快。

剩下的唯一悬而未决的问题是,为什么 array 上的 Python 的 sum 这么慢(它是所有比较函数中最慢的)。这实际上与 Python 的 sum 简单地迭代您传入的任何内容这一事实有关。如果是列表,它会获取存储的 Python 对象,但如果是一维 NumPy 数组,则没有存储Python 对象,只是 C 值,因此 Python&NumPy 必须为每个元素创建一个 Python 对象(numpy.int32numpy.int64),然后必须添加这些 Python 对象。为 C 值创建包装器使其变得非常慢。

此外,使用 Python 整数与标量 numpy.int32 有何影响(包括性能)?例如,对于 a += 1,如果 a 的类型是 Python 整数或 numpy.int32,是否存在行为或性能差异?

我做了一些测试,对于标量的加减法,你绝对应该坚持使用 Python 整数。尽管可能会进行一些缓存,这意味着以下测试可能并不完全具有代表性:

from itertools import repeat

python_integer = 1000
numpy_integer_32 = np.int32(1000)
numpy_integer_64 = np.int64(1000)

def repeatedly_add_one(val):
    for _ in repeat(None, 100000):
        _ = val + 1

%timeit repeatedly_add_one(python_integer)
3.7 ms ± 71.2 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

%timeit repeatedly_add_one(numpy_integer_32)
14.3 ms ± 162 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

%timeit repeatedly_add_one(numpy_integer_64)
18.5 ms ± 494 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)


def repeatedly_sub_one(val):
    for _ in repeat(None, 100000):
        _ = val - 1

%timeit repeatedly_sub_one(python_integer)
3.75 ms ± 236 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%timeit repeatedly_sub_one(numpy_integer_32)
15.7 ms ± 437 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%timeit repeatedly_sub_one(numpy_integer_64)
19 ms ± 834 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

使用 Python 整数进行标量运算比使用 NumPy 标量快 3-6 倍。我没有检查为什么会这样,但我的猜测是 NumPy 标量很少使用并且可能没有针对性能进行优化。

如果您实际执行两个操作数都是 numpy 标量的算术运算,则差异会变得更小:

def repeatedly_add_one(val):
    one = type(val)(1)  # create a 1 with the same type as the input
    for _ in repeat(None, 100000):
        _ = val + one

%timeit repeatedly_add_one(python_integer)
3.88 ms ± 273 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%timeit repeatedly_add_one(numpy_integer_32)
6.12 ms ± 324 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%timeit repeatedly_add_one(numpy_integer_64)
6.49 ms ± 265 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

那么它只慢了 2 倍。


如果您想知道为什么我在这里使用itertools.repeat,而我本来可以简单地使用for _ in range(...)。原因是repeat 更快,因此每个循环产生的开销更少。因为我只对加/减时间感兴趣,所以实际上最好不要让循环开销干扰时间(至少不是那么多)。

【讨论】:

    【解决方案3】:

    请注意,多维 numpy 数组上的 Python 求和只会沿第一个轴执行求和:

    sum(np.array([[[2,3,4],[4,5,6]],[[7,8,9],[10,11,12]]]))
    Out[47]: 
    array([[ 9, 11, 13],
           [14, 16, 18]])
    
    np.sum(np.array([[[2,3,4],[4,5,6]],[[7,8,9],[10,11,12]]]), axis=0)
    Out[48]: 
    array([[ 9, 11, 13],
           [14, 16, 18]])
    
    np.sum(np.array([[[2,3,4],[4,5,6]],[[7,8,9],[10,11,12]]]))
    Out[49]: 81
    

    【讨论】:

      【解决方案4】:

      Numpy 应该更快,尤其是当您的数据已经是一个 numpy 数组时。

      Numpy 数组是标准 C 数组之上的一个薄层。当 numpy sum 对此进行迭代时,它不会进行类型检查并且速度非常快。速度应该与使用标准 C 进行操作相当。

      相比之下,使用 python 的 sum 它必须首先将 numpy 数组转换为 python 数组,然后遍历该数组。它必须进行一些类型检查,而且通常会比较慢。

      python sum 比 numpy sum 慢的确切数量没有很好地定义,因为与在 python 中编写自己的 sum 函数相比,python sum 将是一个稍微优化的函数。

      【讨论】:

      • 它不会“转换” numpy 数组——一个 numpy 数组在 Python 中已经是可迭代的。另一方面,numpy.sum 可能必须将列表转换为 numpy 数组,这将解释 @Akavall 计时的结果。
      • 无论转换是作为数组到数组还是通过对单个项目进行类型转换,您都将在某种程度上转换项目(从/到本机类型),这就是我尝试的重点制作。
      【解决方案5】:

      这是answer post above by Akavall 的扩展。从该答案中,您可以看到np.sumnp.array 对象的执行速度更快,而sumlist 对象的执行速度更快。对此进行扩展:

      在为np.array 对象运行np.sumVs. sumlist 对象运行时,它们似乎并驾齐驱。

      # I'm running IPython
      
      In [1]: x = range(1000) # list object
      
      In [2]: y = np.array(x) # np.array object
      
      In [3]: %timeit sum(x)
      100000 loops, best of 3: 14.1 µs per loop
      
      In [4]: %timeit np.sum(y)
      100000 loops, best of 3: 14.3 µs per loop
      

      在上面,sumnp.array 快一点,尽管有时我看到np.sum 的时间也比14.1 µs 快。但大多数情况下,它是14.3 µs

      【讨论】:

        【解决方案6】:

        如果你使用 sum(),那么它会给出

        a = np.arange(6).reshape(2, 3)
        print(a)
        print(sum(a))
        print(sum(sum(a)))
        print(np.sum(a))
        
        
        >>>
        [[0 1 2]
         [3 4 5]]
        [3 5 7]
        15
        15
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2022-07-15
          • 2023-02-22
          • 2013-01-12
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2015-07-02
          相关资源
          最近更新 更多