![](/img/trans.png)
[英]using numpy broadcast_to create a repeated view of every element in array
[英]Why is repeated numpy array access faster using a single-element view?
我在另一个 SO 线程中看到,可以使用arr[index:index+1]
创建数组arr
的单元素视图。 这对我很有用,因为我需要重复设置一个(可能很大~100k 个条目)数组的几个值。 但在我刚刚使用这种方法之前,我想确保创建视图的额外工作不会花费太多时间。 令人惊讶的是,我发现如果您至少访问一个索引约 10 次,那么使用该视图已经更好了。
此 plot 的数据是通过对两种方法进行定时创建的(在 python 3.10 中):
#!/bin/python3
# https://gist.github.com/SimonLammer/7f27fd641938b4a8854b55a3851921db
from datetime import datetime, timedelta
import numpy as np
import timeit
np.set_printoptions(linewidth=np.inf, formatter={'float': lambda x: format(x, '1.5E')})
def indexed(arr, indices, num_indices, accesses):
s = 0
for index in indices[:num_indices]:
for _ in range(accesses):
s += arr[index]
def viewed(arr, indices, num_indices, accesses):
s = 0
for index in indices[:num_indices]:
v = arr[index:index+1]
for _ in range(accesses):
s += v[0]
return s
N = 11_000 # Setting this higher doesn't seem to have significant effect
arr = np.random.randint(0, N, N)
indices = np.random.randint(0, N, N)
options = [1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765, 10946]
for num_indices in options:
for accesses in options:
print(f"{num_indices=}, {accesses=}")
for func in ['indexed', 'viewed']:
t = np.zeros(5)
end = datetime.now() + timedelta(seconds=2.5)
i = 0
while i < 5 or datetime.now() < end:
t += timeit.repeat(f'{func}(arr, indices, num_indices, accesses)', number=1, globals=globals())
i += 1
t /= i
print(f" {func.rjust(7)}:", t, f"({i} runs)")
这些观察对我来说非常违反直觉。 为什么viewed
速度快于indexed
(每个索引超过 10 次访问)?
编辑 1 :
编辑 2 :
我可以复制 Jérôme Richard 的发现。 罪魁祸首是索引数据类型(python int vs. numpy int):
>>> import timeit
>>> timeit.timeit('arr[i]', setup='import numpy as np; arr = np.random.randint(0, 1000, 1000); i = np.random.randint(0, len(arr), 1)[0]', number=20000000)
1.618339812999693
>>> timeit.timeit('arr[i]', setup='import numpy as np; arr = np.random.randint(0, 1000, 1000); i = np.random.randint(0, len(arr), 1)[0]; i = int(i)', number=20000000)
1.2747555710002416
由于num_indices
对观察到的性能没有显着影响,我们可以通过丢弃该参数(即设置为 1)来简化问题。 由于只有大accesses
很重要,我们还可以通过仅考虑像 10946 这样的大值来简化问题。 在不影响基准的情况下,也可以简化index
的使用。 同样的事情也适用于return
语句。 简化的问题现在是我们得到这个的原因(在 CPython 3.10.5 上重现):
import numpy as np
def indexed(arr, index):
s = 0
for _ in range(10946): s += arr[index]
def viewed(arr, index):
s = 0
v = arr[index:index+1]
for _ in range(10946): s += v[0]
N = 11_000
arr = np.random.randint(0, N, N)
indices = np.random.randint(0, N, N)
# mean ± std. dev. of 7 runs, 1000 loops each
%timeit indexed(arr, indices[0]) # 1.24 ms ± 22.3 µs per loop
%timeit viewed(arr, indices[0]) # 0.99 ms ± 4.34 µs per loop
现在,放缓的根源非常有限。 它只与arr[index]
与v[0]
有关。 同样重要的是要注意arr
和v
基本上属于同一类型,而index
和0
不属于同一类型。 实际上, index
if 类型为np.int64
而0
是 PyLong object。 问题是Numpy 项目类型比内置项目类型慢得多。 简而言之,在这两种情况下,CPython 函数都由 Numpy 调用,但是PyLong
对象的计算速度比基于PyNumber
的通用数据类型(由 CPython 本身)更快。 Numpy 检查也是有序的,因此第一种类型也可以更快。 有关详细信息,请参阅“* 更深入的分析与讨论*”部分。
要解决此问题,您只需将 Numpy 类型转换为内置类型:
import numpy as np
def indexed(arr, index):
s = 0
nativeIndex = int(index) # <------------------------------
for _ in range(10946): s += arr[nativeIndex]
def viewed(arr, index):
s = 0
v = arr[index:index+1]
for _ in range(10946): s += v[0]
N = 11_000
arr = np.random.randint(0, N, N)
indices = np.random.randint(0, N, N)
# mean ± std. dev. of 7 runs, 1000 loops each
%timeit indexed(arr, indices[0]) # 981 µs ± 4.6 µs per loop
%timeit viewed(arr, indices[0]) # 989 µs ± 5.3 µs per loop
# The difference is smaller than the sum of the standard deviations
# so the gap is clearly not statistically significant anymore.
此外,请注意,这两个函数的几乎所有时间都是纯开销。 Numpy 不是为进行标量访问而设计的,而是针对矢量化访问进行了优化。 像v[0]
这样的幼稚表达式会导致处理器完成大量工作:需要解释表达式,需要分配一个新的引用计数 object,需要调用 Numpy 的几个(内部)函数许多开销(包装、动态类型检查、内部迭代器配置)。 低级分析器报告了数十个 function 调用被调用,并且在现代 x86 处理器上应该占用不超过 1 个吞吐量周期的东西浪费了 250-300 个周期。
当计算表达式arr[nativeIndex]
(或v[0]
)时,CPython 解释器最终会调用 Numpy 函数,尤其是通用array_subscript
。 这个 function 调用prepare_index
在操作数中进行一些类型检查。 此 function 使用非内置类型较慢。 一个原因是PyArray_PyIntAsIntp
调用 CPython 类型检查函数,并且用于检查PyLong
类型的代码比“涵盖所有内容”的更通用的替代方法更快(包括np.int64
,引用代码中的注释)。 此外,有级联的检查和类型导致提前中断往往也会导致更快的检查阶段(因为所有 CPython function 调用的累积成本)。 因此,Numpy 开发人员倾向于将更常见的情况放在首位。 话虽这么说,这并不是那么简单,因为检查每种类型的成本是不一样的(所以先放慢一个普通类型的检查速度可能会减慢所有其他情况),并且有时代码中存在依赖关系。 另请注意,CPython 接口阻止 Numpy 进行进一步优化(CPython 在内置函数中所做的)并因此使更通用的类型也慢一些。 事实上,这就是让内置 function 更快的原因(尽管这通常不是唯一的原因)。
相关文章: 为什么np.sum(range(N))
很慢?
更新:我不能再复制这个答案的时间了。 也许我在设置步骤中做了一些改变这些结果的事情; 或者他们只是巧合。
>>> arr = np.random.randint(0, 1000, 1000)
>>> i = 342
>>> def a3(i): return arr[i]
...
>>> def b3(i): return arr[342]
...
>>> def c3(i): return arr[0]
...
>>> t = timeit.repeat('a3(i)', globals=globals(), number=100000000); print(t, np.mean(t), np.median(t))
[17.449311104006483, 17.405843814995023, 17.91914719599299, 18.123263651999878, 18.04744581299019] 17.789002315996914 17.91914719599299
>>> t = timeit.repeat('b3(i)', globals=globals(), number=100000000); print(t, np.mean(t), np.median(t))
[17.55685576199903, 18.099313585989876, 18.032570399998804, 18.153590378991794, 17.628647994992207] 17.894195624394342 18.032570399998804
>>> t = timeit.repeat('c3(i)', globals=globals(), number=100000000); print(t, np.mean(t), np.median(t))
[17.762766532003297, 17.826293045000057, 17.821444382003392, 17.618322997994255, 17.488862683996558] 17.703537928199513 17.762766532003297
时间差异似乎是由加载变量与加载常量引起的。
import numpy as np
import dis
arr = np.random.randint(0, 1000, 1000)
def a3(i):
return arr[i]
def b3(i):
return arr[342]
def c3(i):
return arr[0]
这些函数的区别只是用i
、 342
或0
索引数组的方式。
>>> dis.dis(a3)
2 0 LOAD_GLOBAL 0 (arr)
2 LOAD_FAST 0 (i)
4 BINARY_SUBSCR
6 RETURN_VALUE
>>> dis.dis(b3)
2 0 LOAD_GLOBAL 0 (arr)
2 LOAD_CONST 1 (342)
4 BINARY_SUBSCR
6 RETURN_VALUE
>>> dis.dis(c3)
2 0 LOAD_GLOBAL 0 (arr)
2 LOAD_CONST 1 (0)
4 BINARY_SUBSCR
6 RETURN_VALUE
可变索引比常数索引慢 (~8%),而常数索引 0 仍然快 (~5%)。 访问索引 0 ( c3
) 处的数组比变量索引 ( a3
) 快 (~13%)。
>>> t = timeit.repeat('a3(i)', globals=globals(), number=10000000); print(t, np.mean(t), np.median(t))
[1.4897515250049764, 1.507482559987693, 1.5573357169923838, 1.581711255988921, 1.588776800010237] 1.5450115715968422 1.5573357169923838
>>> t = timeit.repeat('b3(i)', globals=globals(), number=10000000); print(t, np.mean(t), np.median(t))
[1.4514476449985523, 1.427873961001751, 1.4268056689907098, 1.4114146630017785, 1.442651974997716] 1.4320387825981016 1.427873961001751
>>> t = timeit.repeat('c3(i)', globals=globals(), number=10000000); print(t, np.mean(t), np.median(t))
[1.357518576012808, 1.3500928360008402, 1.3615708220022498, 1.376022889991873, 1.3813936790102161] 1.3653197606035974 1.3615708220022498
感谢 u/jtclimb https://www.reddit.com/r/Numpy/comments/wb4p12/comment/ii7q53s/?utm_source=share&utm_medium=web2x&context=3
编辑 1:使用timeit.repeat
的setup
参数反驳了这个假设。
>>> t=timeit.repeat('arr[i]', setup='import numpy as np; arr = np.random.randint(0,10000,1000000); i = 342', number=10000000); print(np.around(t, 5), np.mean(t), np.median(t))
[0.7697 0.76627 0.77007 0.76424 0.76788] 0.7676320286031114 0.7678760859998874
>>> t=timeit.repeat('arr[0]', setup='import numpy as np; arr = np.random.randint(0,10000,1000000); i = 342', number=10000000); print(np.around(t, 5), np.mean(t), np.median(t))
[0.76836 0.76629 0.76794 0.76619 0.7682 ] 0.7673966443951941 0.7679443680099212
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.