为什么 `vectorize` 优于 `frompyfunc`?
Why `vectorize` is outperformed by `frompyfunc`?
Numpy 提供 vectorize
and frompyfunc
具有相似的功能。
正如本 SO-post, vectorize
wraps frompyfunc
中所指出的,并正确处理了 returned 数组的类型,而 frompyfunc
returns 的数组np.object
.
但是,对于所有尺寸,frompyfunc
始终优于 vectorize
10-20%,这也无法用不同的 return 类型来解释。
考虑以下变体:
import numpy as np
def do_double(x):
return 2.0*x
vectorize = np.vectorize(do_double)
frompyfunc = np.frompyfunc(do_double, 1, 1)
def wrapped_frompyfunc(arr):
return frompyfunc(arr).astype(np.float64)
wrapped_frompyfunc
只是将 frompyfunc
的结果转换为正确的类型——正如我们所见,此操作的成本几乎可以忽略不计。
结果如下(蓝线是frompyfunc
):
我预计 vectorize
会有更多的开销 - 但这应该只出现在小尺寸的情况下。另一方面,将 np.object
转换为 np.float64
也在 wrapped_frompyfunc
中完成 - 这仍然快得多。
如何解释这种性能差异?
使用 perfplot-package 生成时序比较的代码(给定上述功能):
import numpy as np
import perfplot
perfplot.show(
setup=lambda n: np.linspace(0, 1, n),
n_range=[2**k for k in range(20,27)],
kernels=[
frompyfunc,
vectorize,
wrapped_frompyfunc,
],
labels=["frompyfunc", "vectorize", "wrapped_frompyfunc"],
logx=True,
logy=False,
xlabel='len(x)',
equality_check = None,
)
注意:对于较小的尺寸,vectorize
的开销要高得多,但这是可以预料的(毕竟它包装了 frompyfunc
):
按照@hpaulj 的提示,我们可以分析vectorize
-函数:
arr=np.linspace(0,1,10**7)
%load_ext line_profiler
%lprun -f np.vectorize._vectorize_call \
-f np.vectorize._get_ufunc_and_otypes \
-f np.vectorize.__call__ \
vectorize(arr)
这表明 100% 的时间花在了 _vectorize_call
:
Timer unit: 1e-06 s
Total time: 3.53012 s
File: python3.7/site-packages/numpy/lib/function_base.py
Function: __call__ at line 2063
Line # Hits Time Per Hit % Time Line Contents
==============================================================
2063 def __call__(self, *args, **kwargs):
...
2091 1 3530112.0 3530112.0 100.0 return self._vectorize_call(func=func, args=vargs)
...
Total time: 3.38001 s
File: python3.7/site-packages/numpy/lib/function_base.py
Function: _vectorize_call at line 2154
Line # Hits Time Per Hit % Time Line Contents
==============================================================
2154 def _vectorize_call(self, func, args):
...
2161 1 85.0 85.0 0.0 ufunc, otypes = self._get_ufunc_and_otypes(func=func, args=args)
2162
2163 # Convert args to object arrays first
2164 1 1.0 1.0 0.0 inputs = [array(a, copy=False, subok=True, dtype=object)
2165 1 117686.0 117686.0 3.5 for a in args]
2166
2167 1 3089595.0 3089595.0 91.4 outputs = ufunc(*inputs)
2168
2169 1 4.0 4.0 0.0 if ufunc.nout == 1:
2170 1 172631.0 172631.0 5.1 res = array(outputs, copy=False, subok=True, dtype=otypes[0])
2171 else:
2172 res = tuple([array(x, copy=False, subok=True, dtype=t)
2173 for x, t in zip(outputs, otypes)])
2174 1 1.0 1.0 0.0 return res
它显示了我在假设中遗漏的部分:双数组完全在预处理步骤中转换为对象数组(这在内存方面不是一件非常明智的事情)。 wrapped_frompyfunc
其他部分类似:
Timer unit: 1e-06 s
Total time: 3.20055 s
File: <ipython-input-113-66680dac59af>
Function: wrapped_frompyfunc at line 16
Line # Hits Time Per Hit % Time Line Contents
==============================================================
16 def wrapped_frompyfunc(arr):
17 1 3014961.0 3014961.0 94.2 a = frompyfunc(arr)
18 1 185587.0 185587.0 5.8 b = a.astype(np.float64)
19 1 1.0 1.0 0.0 return b
当我们查看峰值内存消耗时(例如通过 /usr/bin/time python script.py
),我们会看到 vectorized
版本的内存消耗是 frompyfunc
的两倍,它使用一个更复杂的策略:双数组以大小为 NPY_BUFSIZE
的块(即 8192)处理,因此内存中同时只有 8192 python-floats(24bytes+8byte 指针) (而不是数组中元素的数量,这可能要高得多)。从 OS + 更多缓存未命中中保留内存的成本可能导致更高的 运行 次。
我的收获:
- 可能根本不需要将所有输入转换为对象数组的预处理步骤,因为
frompyfunc
有一种更复杂的方法来处理这些转换。
-
vectorize
和 frompyfunc
都不应该使用,而结果 ufunc
应该在 "real code" 中使用。相反,应该用 C 编写或使用 numba/similar.
在对象数组上调用 frompyfunc
比在双数组上调用需要的时间更少:
arr=np.linspace(0,1,10**7)
a = arr.astype(np.object)
%timeit frompyfunc(arr) # 1.08 s ± 65.8 ms
%timeit frompyfunc(a) # 876 ms ± 5.58 ms
然而,上面的 line-profiler-timings 并没有显示出在对象上使用 ufunc
而不是双打的任何优势:3.089595s 与 3014961.0s。我的怀疑是,这是由于在创建所有对象的情况下更多的缓存未命中,而 L2 缓存中只有 8192 个创建的对象 (256Kb) 是热的。
Numpy 提供 vectorize
and frompyfunc
具有相似的功能。
正如本 SO-post, vectorize
wraps frompyfunc
中所指出的,并正确处理了 returned 数组的类型,而 frompyfunc
returns 的数组np.object
.
但是,对于所有尺寸,frompyfunc
始终优于 vectorize
10-20%,这也无法用不同的 return 类型来解释。
考虑以下变体:
import numpy as np
def do_double(x):
return 2.0*x
vectorize = np.vectorize(do_double)
frompyfunc = np.frompyfunc(do_double, 1, 1)
def wrapped_frompyfunc(arr):
return frompyfunc(arr).astype(np.float64)
wrapped_frompyfunc
只是将 frompyfunc
的结果转换为正确的类型——正如我们所见,此操作的成本几乎可以忽略不计。
结果如下(蓝线是frompyfunc
):
我预计 vectorize
会有更多的开销 - 但这应该只出现在小尺寸的情况下。另一方面,将 np.object
转换为 np.float64
也在 wrapped_frompyfunc
中完成 - 这仍然快得多。
如何解释这种性能差异?
使用 perfplot-package 生成时序比较的代码(给定上述功能):
import numpy as np
import perfplot
perfplot.show(
setup=lambda n: np.linspace(0, 1, n),
n_range=[2**k for k in range(20,27)],
kernels=[
frompyfunc,
vectorize,
wrapped_frompyfunc,
],
labels=["frompyfunc", "vectorize", "wrapped_frompyfunc"],
logx=True,
logy=False,
xlabel='len(x)',
equality_check = None,
)
注意:对于较小的尺寸,vectorize
的开销要高得多,但这是可以预料的(毕竟它包装了 frompyfunc
):
按照@hpaulj 的提示,我们可以分析vectorize
-函数:
arr=np.linspace(0,1,10**7)
%load_ext line_profiler
%lprun -f np.vectorize._vectorize_call \
-f np.vectorize._get_ufunc_and_otypes \
-f np.vectorize.__call__ \
vectorize(arr)
这表明 100% 的时间花在了 _vectorize_call
:
Timer unit: 1e-06 s
Total time: 3.53012 s
File: python3.7/site-packages/numpy/lib/function_base.py
Function: __call__ at line 2063
Line # Hits Time Per Hit % Time Line Contents
==============================================================
2063 def __call__(self, *args, **kwargs):
...
2091 1 3530112.0 3530112.0 100.0 return self._vectorize_call(func=func, args=vargs)
...
Total time: 3.38001 s
File: python3.7/site-packages/numpy/lib/function_base.py
Function: _vectorize_call at line 2154
Line # Hits Time Per Hit % Time Line Contents
==============================================================
2154 def _vectorize_call(self, func, args):
...
2161 1 85.0 85.0 0.0 ufunc, otypes = self._get_ufunc_and_otypes(func=func, args=args)
2162
2163 # Convert args to object arrays first
2164 1 1.0 1.0 0.0 inputs = [array(a, copy=False, subok=True, dtype=object)
2165 1 117686.0 117686.0 3.5 for a in args]
2166
2167 1 3089595.0 3089595.0 91.4 outputs = ufunc(*inputs)
2168
2169 1 4.0 4.0 0.0 if ufunc.nout == 1:
2170 1 172631.0 172631.0 5.1 res = array(outputs, copy=False, subok=True, dtype=otypes[0])
2171 else:
2172 res = tuple([array(x, copy=False, subok=True, dtype=t)
2173 for x, t in zip(outputs, otypes)])
2174 1 1.0 1.0 0.0 return res
它显示了我在假设中遗漏的部分:双数组完全在预处理步骤中转换为对象数组(这在内存方面不是一件非常明智的事情)。 wrapped_frompyfunc
其他部分类似:
Timer unit: 1e-06 s
Total time: 3.20055 s
File: <ipython-input-113-66680dac59af>
Function: wrapped_frompyfunc at line 16
Line # Hits Time Per Hit % Time Line Contents
==============================================================
16 def wrapped_frompyfunc(arr):
17 1 3014961.0 3014961.0 94.2 a = frompyfunc(arr)
18 1 185587.0 185587.0 5.8 b = a.astype(np.float64)
19 1 1.0 1.0 0.0 return b
当我们查看峰值内存消耗时(例如通过 /usr/bin/time python script.py
),我们会看到 vectorized
版本的内存消耗是 frompyfunc
的两倍,它使用一个更复杂的策略:双数组以大小为 NPY_BUFSIZE
的块(即 8192)处理,因此内存中同时只有 8192 python-floats(24bytes+8byte 指针) (而不是数组中元素的数量,这可能要高得多)。从 OS + 更多缓存未命中中保留内存的成本可能导致更高的 运行 次。
我的收获:
- 可能根本不需要将所有输入转换为对象数组的预处理步骤,因为
frompyfunc
有一种更复杂的方法来处理这些转换。 -
vectorize
和frompyfunc
都不应该使用,而结果ufunc
应该在 "real code" 中使用。相反,应该用 C 编写或使用 numba/similar.
在对象数组上调用 frompyfunc
比在双数组上调用需要的时间更少:
arr=np.linspace(0,1,10**7)
a = arr.astype(np.object)
%timeit frompyfunc(arr) # 1.08 s ± 65.8 ms
%timeit frompyfunc(a) # 876 ms ± 5.58 ms
然而,上面的 line-profiler-timings 并没有显示出在对象上使用 ufunc
而不是双打的任何优势:3.089595s 与 3014961.0s。我的怀疑是,这是由于在创建所有对象的情况下更多的缓存未命中,而 L2 缓存中只有 8192 个创建的对象 (256Kb) 是热的。