[...] 我的 [...] 问题是numpy.sum
在 Python 整数列表上使用会比使用 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 列表中。请注意,这是一个对数图,因为基准测试涵盖了非常广泛的值。但是对于定性结果:较低意味着更好。
这表明对于列表,Pythonsum
总是更快,而数组上np.sum
的sum
方法会更快(除了非常短的数组,其中 Pythonsum
更快)。
以防万一您有兴趣将它们相互比较,我还制作了一个包含所有这些的情节:
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 整数可以任意大,因此它不能简单地使用一个C 整数,但它足够接近)。
例如,像[1, 2, 3]
这样的列表(示意性地,我省略了一些细节)存储如下:
然而,NumPy 数组是包含 C 值的 C 数组的包装器(在这种情况下int
或long
取决于 32 位或 64 位并取决于操作系统)。
所以一个像这样的 NumPy 数组np.array([1, 2, 3])
看起来像这样:
接下来要了解的是这些函数是如何工作的:
- Pythons
sum
遍历可迭代对象(在本例中为列表或数组)并添加所有元素。
- NumPys
sum
方法遍历存储的 C 数组并添加这些 C 值,最后将该值包装在 Python 类型中(在本例中为numpy.int32
(or numpy.int64
) 并返回它。
- NumPys
sum
函数将输入转换为一个array
(至少如果它不是一个数组),然后使用 NumPysum
方法。
显然,从 C 数组添加 C 值比添加 Python 对象快得多,这就是 NumPy 函数可以更快的原因(参见上面的第二张图,数组上的 NumPy 函数远远超过了大型数组的 Python 总和)。
但是将 Python 列表转换为 NumPy 数组相对较慢,然后您仍然必须添加 C 值。这就是为什么对于列表,Pythonsum
会更快。
sum
唯一剩下的悬而未决的问题是为什么 Pythonarray
如此缓慢(它是所有比较函数中最慢的)。这实际上与 Python 的 sum 简单地迭代您传入的任何内容这一事实有关。如果是列表,它会获取存储的Python 对象,但如果是一维 NumPy 数组,则没有存储的 Python 对象,只有 C 值,因此 Python&NumPy 必须为每个元素创建一个 Python 对象(annumpy.int32
或numpy.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
速度更快,因此每个循环的开销更少。因为我只对加/减时间感兴趣,所以实际上最好不要让循环开销与时间混淆(至少不是那么多)。