Python 分配给数组时速度慢吗?
Numba似乎是加速数字代码执行的一个很好的解决方案。然而,当数组有赋值时,Numba似乎比标准Python代码慢。考虑这个例子,比较四个备选方案,有/没有NUBA,写入一个数组/标量: (计算特意保持非常简单,以关注问题,即标量赋值与数组单元格赋值) 使用IPython的%timeit来评估我得到的四个备选方案:Python 分配给数组时速度慢吗?,python,arrays,numba,Python,Arrays,Numba,Numba似乎是加速数字代码执行的一个很好的解决方案。然而,当数组有赋值时,Numba似乎比标准Python代码慢。考虑这个例子,比较四个备选方案,有/没有NUBA,写入一个数组/标量: (计算特意保持非常简单,以关注问题,即标量赋值与数组单元格赋值) 使用IPython的%timeit来评估我得到的四个备选方案: In [125]: %timeit fast_sum_arr(arr) 100 loops, best of 3: 10.8 ms per loop In [126]: %time
In [125]: %timeit fast_sum_arr(arr)
100 loops, best of 3: 10.8 ms per loop
In [126]: %timeit sum_arr(arr)
100 loops, best of 3: 4.11 ms per loop
In [127]: %timeit fast_sum_sclr(arr)
100000 loops, best of 3: 10 us per loop
In [128]: %timeit sum_sclr(arr)
100 loops, best of 3: 2.93 ms per loop
未使用Numba编译的sum_arr的速度是使用Numba编译的fast_sum_arr的两倍多。另一方面,使用Numba编译的fast_sum_sclr比使用Numba编译的sum_sclr快两个数量级以上
因此,Numba在加速sum_sclr的任务中表现得非常出色,但实际上使sum_arr的执行速度变慢。sum_sclr和sum_arr之间的唯一区别在于前者分配给标量,而后者分配给数组单元
我不知道是否有任何关系,但我最近在博客上读到以下内容:
“事实证明,当Numba遇到它不直接支持的任何构造时,它会切换到(非常)慢的代码路径。”
博客作者使用if语句而不是Python的max()让Numba执行得更快
对此有什么见解吗
谢谢
FS我对numba了解不多,但如果我们对它在引擎盖下的工作做一些基本假设,我们可以推断出autojit版本较慢的原因,以及如何通过微小的更改来加速它 让我们从总数开始
1 def sum_arr(arr):
2 z = arr.copy()
3 M = len(arr)
4 for i in range(M):
5 z[i] += arr[i]
6
7 return z
非常清楚这里发生了什么,但是让我们选择第5行,它可以重写为
1 a = arr[i]
2 b = z[i]
3 c = a + b
4 z[i] = c
Python将进一步将其嵌入为
1 a = arr.__getitem__(i)
2 b = arr.__getitem__(i)
3 c = a.__add__(b)
4 z.__setitem__(i, c)
a、 b和c都是numpy.int64(或类似)的实例
我怀疑numba正在尝试检查这些项目的日期类型,并将它们转换为一些numba原生数据类型(我在numpy代码中看到的最大减速之一是无意中从python数据类型切换到numpy数据类型)。如果确实是这样的话,numba至少进行了3次转换,2次numpy.int64->native,1次native->numpy.int64,或者更糟的是,使用中间版本(numpy.int64->python int->native(c int))。我怀疑numba会在检查数据类型时增加额外的开销,可能根本不会优化循环。让我们看看如果我们从循环中删除类型更改会发生什么
1 @autojit
2 def fast_sum_arr2(arr):
3 z = arr.tolist()
4 M = len(arr)
5 for i in range(M):
6 z[i] += arr[i]
7
8 return numpy.array(z)
第3行的细微变化是tolist而不是copy,将数据类型更改为Python ints,但第6行仍然有一个numpy.int64->native。让我们重写为,z[i]+=z[i]
1 @autojit
2 def fast_sum_arr3(arr):
3 z = arr.tolist()
4 M = len(arr)
5 for i in range(M):
6 z[i] += z[i]
7
8 return numpy.array(z)
通过所有的更改,我们看到了相当大的加速(尽管它不一定能打败纯python)。当然,arr+arr,就是愚蠢的快
1 import numpy
2 from numba import autojit
3
4 def sum_arr(arr):
5 z = arr.copy()
6 M = len(arr)
7 for i in range(M):
8 z[i] += arr[i]
9
10 return z
11
12 @autojit
13 def fast_sum_arr(arr):
14 z = arr.copy()
15 M = len(arr)
16 for i in range(M):
17 z[i] += arr[i]
18
19 return z
20
21 def sum_arr2(arr):
22 z = arr.tolist()
23 M = len(arr)
24 for i in range(M):
25 z[i] += arr[i]
26
27 return numpy.array(z)
28
29 @autojit
30 def fast_sum_arr2(arr):
31 z = arr.tolist()
32 M = len(arr)
33 for i in range(M):
34 z[i] += arr[i]
35
36 return numpy.array(z)
37
38 def sum_arr3(arr):
39 z = arr.tolist()
40 M = len(arr)
41 for i in range(M):
42 z[i] += z[i]
43
44 return numpy.array(z)
45
46 @autojit
47 def fast_sum_arr3(arr):
48 z = arr.tolist()
49 M = len(arr)
50 for i in range(M):
51 z[i] += z[i]
52
53 return numpy.array(z)
54
55 def sum_arr4(arr):
56 return arr+arr
57
58 @autojit
59 def fast_sum_arr4(arr):
60 return arr+arr
61
62 arr = numpy.arange(1000)
还有时间
In [1]: %timeit sum_arr(arr)
10000 loops, best of 3: 129 us per loop
In [2]: %timeit sum_arr2(arr)
1000 loops, best of 3: 232 us per loop
In [3]: %timeit sum_arr3(arr)
10000 loops, best of 3: 51.8 us per loop
In [4]: %timeit sum_arr4(arr)
100000 loops, best of 3: 3.68 us per loop
In [5]: %timeit fast_sum_arr(arr)
1000 loops, best of 3: 216 us per loop
In [6]: %timeit fast_sum_arr2(arr)
10000 loops, best of 3: 65.6 us per loop
In [7]: %timeit fast_sum_arr3(arr)
10000 loops, best of 3: 56.5 us per loop
In [8]: %timeit fast_sum_arr4(arr)
100000 loops, best of 3: 2.03 us per loop
这里比较慢的是arr.copy()函数,而不是对数组的写访问。证明:
# -*- coding: utf-8 -*-
from numba import autojit
from Timer import Timer
import numpy as np
@autojit
def fast_sum_arr(arr, z):
#z = arr.copy()
M = len(arr)
for i in range(M):
z[i] += arr[i]
return z
def sum_arr(arr, z):
#z = arr.copy()
M = len(arr)
for i in range(M):
z[i] += arr[i]
return z
@autojit
def fast_sum_sclr(arr):
z = 0
M = len(arr)
for i in range(M):
z += arr[i]
return z
def sum_sclr(arr):
z = 0
M = len(arr)
for i in range(M):
z += arr[i]
return z
if __name__ == '__main__':
vec1 = np.ones(1000)
z = vec1.copy()
with Timer() as t0:
for i in range(10000):
pass
print "time for empty loop ", t0.secs
print
with Timer() as t1:
for i in range(10000):
sum_arr(vec1, z)
print "time for sum_arr [µs]: ", (t1.secs-t0.secs) / 10000 * 1e6
with Timer() as t1:
for i in range(10000):
fast_sum_arr(vec1, z)
print "time for fast_sum_arr [µs]: ", (t1.secs-t0.secs) / 10000 * 1e6
with Timer() as t1:
for i in range(10000):
sum_sclr(vec1)
print "time for sum_arr [µs]: ", (t1.secs-t0.secs) / 10000 * 1e6
with Timer() as t1:
for i in range(10000):
fast_sum_sclr(vec1)
print "time for fast_sum_arr [µs]: ", (t1.secs-t0.secs) / 10000 * 1e6
"""
time for empty loop 0.000312089920044
time for sum_arr [µs]: 432.02688694
time for fast_sum_arr [µs]: 7.43598937988
time for sum_arr [µs]: 284.574580193
time for fast_sum_arr [µs]: 5.74610233307
"""
是的,Numba使用惰性初始化,因此第二次调用时速度更快。 对于大型数组,尽管初始化很慢,但numba仍然比没有numba好 尝试以下取消注释不同的b
import time
import numpy as np
from numba import jit, autojit
@autojit
def fast_sum_arr(arr):
z = arr.copy()
M = len(arr)
for i in range(M):
z[i] += arr[i]
return z
def sum_arr(arr):
z = arr.copy()
M = len(arr)
for i in range(M):
z[i] += arr[i]
return z
@autojit
def fast_sum_sclr(arr):
z = 0
M = len(arr)
for i in range(M):
z += arr[i]
return z
def sum_sclr(arr):
z = 0
M = len(arr)
for i in range(M):
z += arr[i]
return z
b = np.arange(100)
# b = np.arange(1000000)
# b = np.arange(100000000)
print('Vector of len {}\n'.format(len(b)))
print('Sum ARR:\n')
time1 = time.time()
sum_arr(b)
time2 = time.time()
print('No numba: {}'.format(time2 - time1))
time1 = time.time()
fast_sum_arr(b)
time2 = time.time()
print('Numba first time: {}'.format(time2 - time1))
time1 = time.time()
fast_sum_arr(b)
time2 = time.time()
print('Numba second time: {}'.format(time2 - time1))
print('\nSum SCLR:\n')
time1 = time.time()
sum_sclr(b)
time2 = time.time()
print('No numba: {}'.format(time2 - time1))
time1 = time.time()
fast_sum_sclr(b)
time2 = time.time()
print('Numba first time: {}'.format(time2 - time1))
time1 = time.time()
fast_sum_sclr(b)
time2 = time.time()
print('Numba second time: {}'.format(time2 - time1))
在我使用python 3的系统上,numba 0.34.0
"""
Vector of len 100
Sum ARR:
No numba: 7.414817810058594e-05
Numba first time: 0.07130813598632812
Numba second time: 3.814697265625e-06
Sum SCLR:
No numba: 2.6941299438476562e-05
Numba first time: 0.05761408805847168
Numba second time: 1.4066696166992188e-05
"""
及
及
有趣的是,第一次调用和第二次调用之间的计算时间差随着数组大小的增加而减小。我不知道它为什么会这样工作。我不明白你的循环应该做什么。它不是有效地
z[1:][+=arr[1:][/code>,还是因为z
和r
具有相同的值,z[1:][*=2
?我希望这比任何显式循环快得多,但我不一定希望编译器能够告诉我。有趣的见解。在我的测试中,我得到了有趣的见解。在我的测试中,我发现非编译函数仍然优于编译函数。差异不是很显著。在标量情况下(快和sclr)和矢量情况下(快和arrX)的加速度之间的显著差异仍然无法解释。视错觉。速度更快的事实取决于numba使用惰性初始化的事实。在同一会话中第一次调用函数时速度较慢,然后从第二次调用到第10000次调用时速度较快。因此,第一个速度较慢,在平均值中消失。尝试使用1而不是10000来调用它,并尝试在函数内部使用copy来调用它。您将看到copy()的位置不是瓶颈。
"""
Vector of len 100
Sum ARR:
No numba: 7.414817810058594e-05
Numba first time: 0.07130813598632812
Numba second time: 3.814697265625e-06
Sum SCLR:
No numba: 2.6941299438476562e-05
Numba first time: 0.05761408805847168
Numba second time: 1.4066696166992188e-05
"""
"""
Vector of len 1000000
Sum ARR:
No numba: 0.3144559860229492
Numba first time: 0.07181787490844727
Numba second time: 0.0014197826385498047
Sum SCLR:
No numba: 0.15929198265075684
Numba first time: 0.05956888198852539
Numba second time: 0.00037789344787597656
"""
"""
Vector of len 100000000
Sum ARR:
No numba: 30.345629930496216
Numba first time: 0.7232880592346191
Numba second time: 0.586756706237793
Sum SCLR:
No numba: 16.271318912506104
Numba first time: 0.11036324501037598
Numba second time: 0.06010794639587402
"""