为什么Python数组的numpy操作比向量化的数组快

2024-04-19 06:47:47 发布

您现在位置:Python中文网/ 问答频道 /正文

我需要通过设置3D数据数组的阈值来创建布尔掩码:数据小于可接受下限或数据大于可接受上限的位置的掩码必须设置为True(否则False)。简洁地说:

mask = (data < low) or (data > high)

我有两个版本的代码来执行这个操作:一个直接处理numpy中的整个3D数组,而另一个方法在数组的片上循环。与我的预期相反,第二种方法似乎比第一种方法快。为什么?在

^{pr2}$

首先,让我们确保两种方法产生相同的结果:

In [8]: np.all(method1(arr, 0.2, 0.8) == method2(arr, 0.2, 0.8))
Out[8]: True

现在一些计时测试:

In [9]: %timeit method1(arr, 0.2, 0.8)
14.4 ms ± 111 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

In [10]: %timeit method2(arr, 0.2, 0.8)
11.5 ms ± 241 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

这是怎么回事?在


编辑1:在较旧的环境中观察到类似的行为:

In [3]: print(sys.version)
2.7.13 |Continuum Analytics, Inc.| (default, Dec 20 2016, 23:05:08) 
[GCC 4.2.1 Compatible Apple LLVM 6.0 (clang-600.0.57)]

In [4]: print(np.__version__)
1.11.3

In [9]: %timeit method1(arr, 0.2, 0.8)
100 loops, best of 3: 14.3 ms per loop

In [10]: %timeit method2(arr, 0.2, 0.8)
100 loops, best of 3: 13 ms per loop

Tags: of数据方法inlooptrue数组ms
2条回答

优于两种方法

在方法一中,访问数组两次。如果不适合缓存,数据将从RAM中读取两次,从而降低性能。此外,还可以创建注释中提到的临时数组。在

方法2对缓存更友好,因为您要访问数组的较小部分两次,这很可能适合缓存。缺点是循环速度慢,函数调用也比较慢。在

为了在这里获得良好的性能,建议编译代码,这可以使用cython或numba来完成。由于cython版本需要更多的工作(注释,需要一个单独的编译器),我将展示如何使用Numba来完成这项工作。在

import numba as nb
@nb.njit(fastmath=True, cache=True)
def method3(arr, low, high):
  out = np.empty(arr.shape, dtype=nb.boolean)
  for i in range(arr.shape[0]):
    for j in range(arr.shape[1]):
      for k in range(arr.shape[2]):
        out[i,j,k]=arr[i,j,k] < low or arr[i,j,k] > high
  return out

在我的电脑上,使用arr = np.random.random((10, 1000, 1000))这比你的方法1提高了2倍,比你的方法2高出50%(corei7-4771,python3.5,windows)

这只是一个简单的例子,在比较复杂的代码上,你可以利用SIMD,而并行处理这也很容易使用,其性能增益可以大很多。对于非编译代码,矢量化通常是最好的,但并不总是(如图所示),但它总是会导致一个糟糕的缓存行为,如果您正在访问的数据块至少不适合三级缓存,则会导致性能不佳。在其他一些问题上,如果数据不能放入更小的一级缓存或二级缓存,那么性能也会受到影响。另一个优点是在调用这个函数的njited函数中自动内联小njited函数。在

在我自己的测试中,表现上的差异比你的问题更明显。在增加arr数据的第二和第三维度后,这种差异仍然可以清晰地观察到。它在注释掉两个比较函数中的一个(greater_equal或{})之后仍然可以观察到,这意味着我们可以排除这两个函数之间的某种奇怪的相互作用。在

通过将这两种方法的实现更改为以下,我可以显著减少性能上的显著差异(但不能完全消除它):

def method1(arr, low, high):
    out = np.empty(arr.shape, dtype=np.bool)
    high = np.ones_like(arr) * high
    low = np.ones_like(arr) * low
    np.greater_equal(arr, high, out)
    np.logical_or(out, arr < low, out)
    return out

def method2(arr, low, high):
    out = np.empty(arr.shape, dtype=np.bool)
    high = np.ones_like(arr) * high
    low = np.ones_like(arr) * low
    for k in range(arr.shape[0]):
        a = arr[k]
        o = out[k]
        h = high[k]
        l = low[k]
        np.greater_equal(a, h, o)
        np.logical_or(o, a < l, o)
    return out

我假设,当向这些numpy函数提供high或{}作为标量时,它们可能首先在内部创建一个填充了该标量的正确形状的numpy数组。当我们在函数外手动执行此操作时,在这两种情况下,只对完整形状执行一次操作,性能差异就变得不那么明显了。这意味着,无论出于什么原因(可能是缓存?),创建一个用相同常量填充的大数组可能比用相同常量创建k更小的数组的效率要低(正如在原始问题中实现method2自动完成的那样)。在


注意:除了缩小性能差距外,这也会使两种方法的性能差得多(对第二种方法的影响比第一种方法更严重)。因此,虽然这可能会给我们一些问题的线索,但它似乎并不能解释一切。在


编辑

这是method2的新版本,我们现在每次都在循环中手动预创建更小的数组,就像我怀疑在numpy中的原始实现中发生的一样:

^{pr2}$

这个版本确实比我上面的版本快得多(确认在循环内创建许多更小的数组比在循环外创建一个大数组更有效),但是仍然比问题中的原始实现慢。在

假设这些numpy函数确实是先将标量边界转换成这些类型的数组,最后一个函数与问题中的函数之间的性能差异可能是由于Python中创建数组(我的实现)与本机创建数组(原始实现)

相关问题 更多 >