numpy 为什么'vectorize'的性能优于'fromyfunction `?

gwbalxhn  于 2022-11-29  发布在  其他
关注(0)|答案(2)|浏览(122)

Numpy提供具有类似功能的vectorizefrompyfunc
正如SO-post中所指出的,vectorize Package frompyfunc并正确处理返回数组的类型,而frompyfunc返回np.object的数组。
然而,对于所有大小,frompyfunc的性能始终比vectorize高10 - 20%,这也不能用不同的返回类型来解释。
考虑以下变体:

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,  
    )

NB:对于较小的大小,vectorize的开销要高得多,但这是意料之中的(毕竟它 Package 了frompyfunc):

ajsxfq5m

ajsxfq5m1#

按照@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-float(24字节+8字节指针)(而不是数组中的元素数,后者可能更高)。从操作系统保留内存的成本+更多的缓存未命中可能导致更长的运行时间。
我的收获:

  • 可能根本不需要将所有输入转换成对象数组的预处理步骤,因为X1 M7 N1 X具有处理这些转换的甚至更复杂的方式。
  • vectorizefrompyfunc都不应该被使用,当得到的ufunc应该被用在“真实的代码”中时。相反,应该用C或者使用numba/similar来写它。

在object-array上调用frompyfunc所需的时间比在double数组上少:

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

但是,上面的行分析器计时并没有显示出对对象使用ufunc而不是对双精度数使用ufunc的任何优势:3.089595s与3014961.0s。我的怀疑是,这是由于在创建所有对象的情况下缓存未命中更多,而在二级缓存中只有8192个创建的对象(256 Kb)是热的。

kyks70gy

kyks70gy2#

这个问题完全没有意义。如果速度是问题的关键,那么无论是矢量化还是frompyfunction都不是答案。与更快的方法相比,它们之间的任何速度差异都显得微不足道。
我发现这个问题想知道为什么frompyfunc破坏了我的代码(它返回对象),而vectorize工作(它返回我告诉它做什么),并发现人们谈论速度。
现在,在2020年代,numba/jit是可用的,它吹任何速度优势的frompyfunc清洁出水。
我编写了一个toy应用程序,从另一个应用程序返回一个大型的np.uint8数组,并得到了以下结果。

pure python                      200 ms
vectorize                         58 ms
frompyfunc + cast back to uint8   53 ms   
np.empty + numba/njit             55 us (4 cores, 100 us single core)

因此,速度比numpy快1000倍,比python快4000倍
如果有人觉得麻烦的话,我可以把代码贴出来。编写njit版本的代码只不过是在python函数前面添加一行@njit,所以你不需要太过努力。
这比用vectorize封装函数要不方便,因为你必须手动编写numpy数组的循环,但它避免了编写外部C函数。你需要用python的numpy/C类子集编写,避免使用python对象。
也许我在这里对numpy太苛刻了,要求它对一个纯python函数进行矢量化。那么,如果我对numpy的原生数组函数(如min)进行基准测试,会怎样呢?
令人惊讶的是,在np.uint8的385 x360数组上,使用numba/jit比使用np.min获得了10倍的速度提升。np.min(array)为230 us是基准。使用单核时,Numba达到了60 us,使用所有四核时,达到了22 us。

相关问题