【问题标题】:NumPy: function for simultaneous max() and min()NumPy:同时 max() 和 min() 的函数
【发布时间】:2012-08-30 15:39:23
【问题描述】:

numpy.amax() 将在数组中找到最大值,numpy.amin() 对最小值执行相同的操作。如果我想同时找到最大值和最小值,我必须调用这两个函数,这需要将(非常大的)数组传递两次,这似乎很慢。

numpy API 中是否有一个函数可以只通过一次数据就找到最大值和最小值?

【问题讨论】:

  • 多大才算很大?如果我有时间,我将运行一些测试,将 fortran 实现与 amaxamin 进行比较
  • 我承认“非常大”是主观的。就我而言,我说的是几 GB 的数组。
  • 那是相当大的。我编写了一个例子来用fortran计算它(即使你不知道fortran,它应该很容易理解代码)。从 fortran 运行它与通过 numpy 运行它确实有所不同。 (大概,你应该能够从 C 中获得相同的性能......)我不确定——我想我们需要一个 numpy 开发人员来评论为什么我的函数比他们的函数执行得这么好......跨度>
  • 当然,这并不是一个新奇的想法。例如,boost minmax 库 (C++) 提供了我正在寻找的算法的实现。
  • 不是对所问问题的真正答案,但可能对这个线程上的人感兴趣。询问 NumPy 是否将 minmax 添加到问题库 (github.com/numpy/numpy/issues/9836)。

标签: python numpy


【解决方案1】:

numpy API 中是否有一个函数可以只通过一次数据就找到最大值和最小值?

没有。在撰写本文时,还没有这样的功能。 (是的,如果有这样的函数,它的性能将显着优于在一个大数组上依次调用numpy.amin()numpy.amax()。)

【讨论】:

  • 如果有人像我一样想知道,是否只使用两个索引进行就地部分排序:x.partition([0, x.size-1]) 会比 x.min()x.max() 的两个单独调用更快,那么不,它是不是。两个单独的电话总是更快。我测试了x 的以下大小:[1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8] 并测量了分离到部分的相对时间:[35.53%, 5.10%, 53.33%, 36.89%, 37.65 %, 36.03%, 42.89%]。因此,如果您的数据向量有 1 亿个样本大小,与部分排序相比,单独调用 min 和 max 将仅使用 42.89% 的时间。
【解决方案2】:

您可以使用Numba,这是一个使用 LLVM 的 NumPy 感知动态 Python 编译器。结果实现非常简单明了:

import numpy
import numba


@numba.jit
def minmax(x):
    maximum = x[0]
    minimum = x[0]
    for i in x[1:]:
        if i > maximum:
            maximum = i
        elif i < minimum:
            minimum = i
    return (minimum, maximum)


numpy.random.seed(1)
x = numpy.random.rand(1000000)
print(minmax(x) == (x.min(), x.max()))

它也应该比 Numpy 的 min() &amp; max() 实现更快。所有这些都无需编写一行 C/Fortran 代码。

进行您自己的性能测试,因为它始终取决于您的架构、您的数据、您的包版本...

【讨论】:

  • > 它也应该比 Numpy 的 min() & max() 实现更快我认为这是不对的。 numpy 不是原生 python - 它是 C. ``` x = numpy.random.rand(10000000) t = time() for i in range(1000): minmax(x) print('numba', time() - t) t = time() for i in range(1000): x.min() x.max() print('numpy', time() - t) ``` 结果:('numba', 10.299750089645386 ) ('numpy', 9.898081064224243)
  • @AuthmanApatira:是的,基准测试总是这样,这就是为什么我说“应该”(更快)和“做你自己的性能测试,因为它始终取决于您的架构、您的数据...”。就我而言,我尝试了 3 台计算机并得到了相同的结果(Numba 比 Numpy 快),但是在您的计算机中结果可能会有所不同...您是否尝试在基准测试之前执行一次 numba 函数以确保它是JIT 编译?另外,如果您使用ipython,为简单起见,我建议您使用%timeit whatever_code() 来衡量执行时间。
  • @AuthmanApatira:无论如何,我试图用这个答案展示的是,有时 Python 代码(在这种情况下是用 Numba 编译的 JIT)可以和最快的 C 编译库一样快(在至少我们谈论的是同一个数量级),考虑到我们只写了纯 Python 代码,这令人印象深刻,你不同意吗? ^^
  • 刚刚遇到这个问题,在实际情况下并不重要,但elif 允许您的最小值大于最大值。例如,对于长度为 1 的数组,最大值将是该值,而最小值是 + 无穷大。一次性的代码没什么大不了的,但深入生产野兽的腹部并不是什么好代码。
  • @MikeWilliamson 您完全正确,感谢您指出这一点!我用更好的minimum/maximum 初始值更新了我的答案。 ^^
【解决方案3】:

如果对您有用的话,有一个查找(最大-最小)函数,称为 numpy.ptp

>>> import numpy
>>> x = numpy.array([1,2,3,4,5,6])
>>> x.ptp()
5

但我认为没有办法通过一次遍历找到最小值和最大值。

编辑: ptp just calls min and max under the hood

【讨论】:

  • 这很烦人,因为大概 ptp 的实现方式必须跟踪最大值和最小值!
  • 或者它可能只是调用 max 和 min,不确定
  • @hayden 原来 ptp 只是调用 max 和 min
  • 那是掩码数组代码;主要的 ndarray 代码在 C 中。但事实证明,C 代码也对数组进行了两次迭代:github.com/numpy/numpy/blob/…
【解决方案4】:

没有人提到numpy.percentile,所以我想我会的。如果您要求[0, 100] 百分位数,它将为您提供一个包含两个元素的数组,即最小值(第 0 个百分位)和最大值(第 100 个百分位)。

但是,它不能满足 OP 的目的:它并不分别比 min 和 max 快。这可能是由于某些机制允许非极端百分位数(一个更难的问题,应该需要更长的时间)。

In [1]: import numpy

In [2]: a = numpy.random.normal(0, 1, 1000000)

In [3]: %%timeit
   ...: lo, hi = numpy.amin(a), numpy.amax(a)
   ...: 
100 loops, best of 3: 4.08 ms per loop

In [4]: %%timeit
   ...: lo, hi = numpy.percentile(a, [0, 100])
   ...: 
100 loops, best of 3: 17.2 ms per loop

In [5]: numpy.__version__
Out[5]: '1.14.4'

如果只请求[0, 100],Numpy 的未来版本可能会添加一个特殊情况来跳过正常的百分位数计算。在不向界面添加任何内容的情况下,有一种方法可以在一次调用中向 Numpy 询问最小值和最大值(与接受的答案中所说的相反),但是库的标准实现并没有利用这种情况来实现它值得。

【讨论】:

    【解决方案5】:

    只是想对可以预期的数字有一些想法,考虑到以下方法:

    import numpy as np
    
    
    def extrema_np(arr):
        return np.max(arr), np.min(arr)
    
    import numba as nb
    
    
    @nb.jit(nopython=True)
    def extrema_loop_nb(arr):
        n = arr.size
        max_val = min_val = arr[0]
        for i in range(1, n):
            item = arr[i]
            if item > max_val:
                max_val = item
            elif item < min_val:
                min_val = item
        return max_val, min_val
    
    import numba as nb
    
    
    @nb.jit(nopython=True)
    def extrema_while_nb(arr):
        n = arr.size
        odd = n % 2
        if not odd:
            n -= 1
        max_val = min_val = arr[0]
        i = 1
        while i < n:
            x = arr[i]
            y = arr[i + 1]
            if x > y:
                x, y = y, x
            min_val = min(x, min_val)
            max_val = max(y, max_val)
            i += 2
        if not odd:
            x = arr[n]
            min_val = min(x, min_val)
            max_val = max(x, max_val)
        return max_val, min_val
    
    %%cython -c-O3 -c-march=native -a
    #cython: language_level=3, boundscheck=False, wraparound=False, initializedcheck=False, cdivision=True, infer_types=True
    
    
    import numpy as np
    
    
    cdef void _extrema_loop_cy(
            long[:] arr,
            size_t n,
            long[:] result):
        cdef size_t i
        cdef long item, max_val, min_val
        max_val = arr[0]
        min_val = arr[0]
        for i in range(1, n):
            item = arr[i]
            if item > max_val:
                max_val = item
            elif item < min_val:
                min_val = item
        result[0] = max_val
        result[1] = min_val
    
    
    def extrema_loop_cy(arr):
        result = np.zeros(2, dtype=arr.dtype)
        _extrema_loop_cy(arr, arr.size, result)
        return result[0], result[1]
    
    %%cython -c-O3 -c-march=native -a
    #cython: language_level=3, boundscheck=False, wraparound=False, initializedcheck=False, cdivision=True, infer_types=True
    
    
    import numpy as np
    
    
    cdef void _extrema_while_cy(
            long[:] arr,
            size_t n,
            long[:] result):
        cdef size_t i, odd
        cdef long x, y, max_val, min_val
        max_val = arr[0]
        min_val = arr[0]
        odd = n % 2
        if not odd:
            n -= 1
        max_val = min_val = arr[0]
        i = 1
        while i < n:
            x = arr[i]
            y = arr[i + 1]
            if x > y:
                x, y = y, x
            min_val = min(x, min_val)
            max_val = max(y, max_val)
            i += 2
        if not odd:
            x = arr[n]
            min_val = min(x, min_val)
            max_val = max(x, max_val)
        result[0] = max_val
        result[1] = min_val
    
    
    def extrema_while_cy(arr):
        result = np.zeros(2, dtype=arr.dtype)
        _extrema_while_cy(arr, arr.size, result)
        return result[0], result[1]
    

    extrema_loop_*() 方法类似于here 提出的方法,而extrema_while_*() 方法基于here 的代码)

    以下时间安排:

    表示extrema_while_*() 是最快的,extrema_while_nb() 是最快的。在任何情况下,extrema_loop_nb()extrema_loop_cy() 解决方案的性能也优于仅 NumPy 的方法(分别使用 np.max()np.min())。

    最后,请注意,这些都没有np.min()/np.max() 灵活(在 n-dim 支持、axis 参数等方面)。

    (完整代码可here

    【讨论】:

    • 如果使用 @njit(fastmath=True) extrema_while_nb 似乎可以额外获得 10% 的速度
    【解决方案6】:

    我不认为将数组传递两次是一个问题。考虑以下伪代码:

    minval = array[0]
    maxval = array[0]
    for i in array:
        if i < minval:
           minval = i
        if i > maxval:
           maxval = i
    

    虽然这里只有 1 个循环,但仍有 2 个检查。 (而不是有 2 个循环,每个循环 1 个检查)。实际上,您唯一节省的就是 1 个循环的开销。如果数组真的像你说的那样大,那么与实际循环的工作负载相比,这个开销很小。 (请注意,这都是在 C 中实现的,因此循环或多或少是免费的)。


    编辑 对支持我并信任我的 4 位表示抱歉。你绝对可以优化它。

    这里有一些 fortran 代码,可以通过 f2py 编译成 python 模块(也许 Cython 大师可以过来并与优化的 C 版本进行比较......):

    subroutine minmax1(a,n,amin,amax)
      implicit none
      !f2py intent(hidden) :: n
      !f2py intent(out) :: amin,amax
      !f2py intent(in) :: a
      integer n
      real a(n),amin,amax
      integer i
    
      amin = a(1)
      amax = a(1)
      do i=2, n
         if(a(i) > amax)then
            amax = a(i)
         elseif(a(i) < amin) then
            amin = a(i)
         endif
      enddo
    end subroutine minmax1
    
    subroutine minmax2(a,n,amin,amax)
      implicit none
      !f2py intent(hidden) :: n
      !f2py intent(out) :: amin,amax
      !f2py intent(in) :: a
      integer n
      real a(n),amin,amax
      amin = minval(a)
      amax = maxval(a)
    end subroutine minmax2
    

    通过以下方式编译:

    f2py -m untitled -c fortran_code.f90
    

    现在我们可以测试它了:

    import timeit
    
    size = 100000
    repeat = 10000
    
    print timeit.timeit(
        'np.min(a); np.max(a)',
        setup='import numpy as np; a = np.arange(%d, dtype=np.float32)' % size,
        number=repeat), " # numpy min/max"
    
    print timeit.timeit(
        'untitled.minmax1(a)',
        setup='import numpy as np; import untitled; a = np.arange(%d, dtype=np.float32)' % size,
        number=repeat), '# minmax1'
    
    print timeit.timeit(
        'untitled.minmax2(a)',
        setup='import numpy as np; import untitled; a = np.arange(%d, dtype=np.float32)' % size,
        number=repeat), '# minmax2'
    

    结果对我来说有点惊人:

    8.61869883537 # numpy min/max
    1.60417699814 # minmax1
    2.30169081688 # minmax2
    

    我不得不说,我并不完全理解它。仅比较 np.minminmax1minmax2 仍然是一场失败的战斗,所以这不仅仅是一个内存问题......

    notes -- 将大小增加10**a 的系数并将重复次数减少10**a 的系数(保持问题大小不变)确实会改变性能,但并非以看似一致的方式这表明在 python 中内存性能和函数调用开销之间存在一些相互作用。即使在 fortran 中比较一个简单的 min 实现,也比 numpy 的实现要高大约 2 ...

    【讨论】:

    • 单遍的优点是内存效率。特别是如果您的数组大到可以换出,这可能是巨大的。
    • 不太对,它几乎是一半的速度,因为对于这种数组,内存速度通常是限制因素,所以它可以是一半的速度......
    • 您并不总是需要两次检查。如果i &lt; minval 为真,则i &gt; maxval 始终为假,因此当第二个ifelif 替换时,您平均每次迭代只需进行1.5 次检查。
    • 小记:我怀疑 Cython 是获得最优化的 Python 可调用 C 模块的方法。 Cython 的目标是成为一种类型注释的 Python,然后将其机器翻译为 C,而 f2py 只是包装了手动编码的 Fortran,以便它可以被 Python 调用。 “更公平”的测试可能是手动编码 C,然后使用 f2py (!) 为 Python 包装它。如果您允许使用 C++,那么 Shed Skin 可能是平衡编码易用性和性能的最佳选择。
    • 从 numpy 1.8 开始,min 和 max 在 amd64 平台上被矢量化,在我的 core2duo 上,numpy 的性能和这个 fortran 代码一样好。但是,如果数组超过较大 cpu 缓存的大小,则单次传递将是有利的。
    【解决方案7】:

    通常,您可以通过一次处理两个元素并仅将较小的元素与临时最小值进行比较,将较大的元素与临时最大值进行比较,从而减少 minmax 算法的比较量。与幼稚的方法相比,平均而言,只需要 3/4 的比较。

    这可以用 c 或 fortran(或任何其他低级语言)实现,并且在性能方面几乎是无与伦比的。我使用 来说明原理并获得一个非常快速、独立于 dtype 的实现:

    import numba as nb
    import numpy as np
    
    @nb.njit
    def minmax(array):
        # Ravel the array and return early if it's empty
        array = array.ravel()
        length = array.size
        if not length:
            return
    
        # We want to process two elements at once so we need
        # an even sized array, but we preprocess the first and
        # start with the second element, so we want it "odd"
        odd = length % 2
        if not odd:
            length -= 1
    
        # Initialize min and max with the first item
        minimum = maximum = array[0]
    
        i = 1
        while i < length:
            # Get the next two items and swap them if necessary
            x = array[i]
            y = array[i+1]
            if x > y:
                x, y = y, x
            # Compare the min with the smaller one and the max
            # with the bigger one
            minimum = min(x, minimum)
            maximum = max(y, maximum)
            i += 2
    
        # If we had an even sized array we need to compare the
        # one remaining item too.
        if not odd:
            x = array[length]
            minimum = min(x, minimum)
            maximum = max(x, maximum)
    
        return minimum, maximum
    

    这绝对比 Peque 提出的幼稚方法快:

    arr = np.random.random(3000000)
    assert minmax(arr) == minmax_peque(arr)  # warmup and making sure they are identical 
    %timeit minmax(arr)            # 100 loops, best of 3: 2.1 ms per loop
    %timeit minmax_peque(arr)      # 100 loops, best of 3: 2.75 ms per loop
    

    正如预期的那样,新的 minmax 实现只需要大约 3/4 的简单实现时间 (2.1 / 2.75 = 0.7636363636363637)

    【讨论】:

    • 在我的机器上,你的解决方案并不比 Peque 的快。麻木 0.33.
    • @johnzwinck 您是否在我的回答中运行了基准测试,它是不同的?如果可以的话可以分享一下吗?但这是可能的:我也注意到新版本中的一些回归。
    • 我运行了你的基准测试。您的解决方案和@Peque 的时间几乎相同(约 2.8 毫秒)。
    • @JohnZwinck 这很奇怪,我刚刚再次测试了它,在我的电脑上它肯定更快。也许这与依赖于硬件的 numba 和 LLVM 有关。
    • 我现在在另一台机器上(一个强大的工作站)尝试了你的 2.4 毫秒,而 Peque 的 2.6 毫秒。所以,一个小小的胜利。
    【解决方案8】:

    previous answer 的启发,我编写了 numba 实现,从二维数组返回轴 = 0 的 minmax。它比调用 numpy min/max 快约 5 倍。 也许有人会觉得它有用。

    from numba import jit
    
    @jit
    def minmax(x):
        """Return minimum and maximum from 2D array for axis=0."""    
        m, n = len(x), len(x[0])
        mi, ma = np.empty(n), np.empty(n)
        mi[:] = ma[:] = x[0]
        for i in range(1, m):
            for j in range(n):
                if x[i, j]>ma[j]: ma[j] = x[i, j]
                elif x[i, j]<mi[j]: mi[j] = x[i, j]
        return mi, ma
    
    x = np.random.normal(size=(256, 11))
    mi, ma = minmax(x)
    
    np.all(mi == x.min(axis=0)), np.all(ma == x.max(axis=0))
    # (True, True)
    
    
    %timeit x.min(axis=0), x.max(axis=0) 
    # 15.9 µs ± 9.4 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
    %timeit minmax(x) 
    # 2.62 µs ± 31.3 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
    

    【讨论】:

    • 如果您避免使用mi[:] = ma[:] = x[0] 并将这部分代码集成到循环的 else 语句中,它可能会更快一些。 ma[:] = x[0] 无非是对数组 ma 的一个完整循环,这是可以避免的。
    【解决方案9】:

    乍一看,numpy.histogram 似乎可以做到这一点:

    count, (amin, amax) = numpy.histogram(a, bins=1)
    

    ...但是如果您查看该函数的source,它只会独立调用a.min()a.max(),因此无法避免此问题中解决的性能问题。 :-(

    同样,scipy.ndimage.measurements.extrema 看起来也是一种可能性,但它也只是单独调用 a.min()a.max()

    【讨论】:

    • np.histogram 并不总是适用于此,因为返回的 (amin, amax) 值是 bin 的最小值和最大值。例如,如果我有a = np.zeros(10)np.histogram(a, bins=1) 返回(array([10]), array([-0.5, 0.5]))。在这种情况下,用户正在寻找(amin, amax) = (0, 0)。
    【解决方案10】:

    无论如何,这对我来说是值得的,所以我会在这里为任何感兴趣的人提出最困难和最不优雅的解决方案。我的解决方案是在 C++ 中实现一个多线程的 min-max in one pass 算法,并使用它来创建一个 Python 扩展模块。这项工作需要一些开销来学习如何使用 Python 和 NumPy C/C++ API,在这里我将展示代码并为希望走这条路的人提供一些小的解释和参考。

    多线程最小值/最大值

    这里没有什么太有趣的了。数组被分成大小为length / workers 的块。为future 中的每个块计算最小/最大值,然后扫描全局最小值/最大值。

        // mt_np.cc
        //
        // multi-threaded min/max algorithm
    
        #include <algorithm>
        #include <future>
        #include <vector>
    
        namespace mt_np {
    
        /*
         * Get {min,max} in interval [begin,end)
         */
        template <typename T> std::pair<T, T> min_max(T *begin, T *end) {
          T min{*begin};
          T max{*begin};
          while (++begin < end) {
            if (*begin < min) {
              min = *begin;
              continue;
            } else if (*begin > max) {
              max = *begin;
            }
          }
          return {min, max};
        }
    
        /*
         * get {min,max} in interval [begin,end) using #workers for concurrency
         */
        template <typename T>
        std::pair<T, T> min_max_mt(T *begin, T *end, int workers) {
          const long int chunk_size = std::max((end - begin) / workers, 1l);
          std::vector<std::future<std::pair<T, T>>> min_maxes;
          // fire up the workers
          while (begin < end) {
            T *next = std::min(end, begin + chunk_size);
            min_maxes.push_back(std::async(min_max<T>, begin, next));
            begin = next;
          }
          // retrieve the results
          auto min_max_it = min_maxes.begin();
          auto v{min_max_it->get()};
          T min{v.first};
          T max{v.second};
          while (++min_max_it != min_maxes.end()) {
            v = min_max_it->get();
            min = std::min(min, v.first);
            max = std::max(max, v.second);
          }
          return {min, max};
        }
        }; // namespace mt_np
    

    Python 扩展模块

    这就是事情开始变得丑陋的地方...在 Python 中使用 C++ 代码的一种方法是实现扩展模块。可以使用distutils.core 标准模块构建和安装此模块。 Python 文档中涵盖了这方面的完整描述:https://docs.python.org/3/extending/extending.html注意:当然还有其他方法可以获得类似的结果,引用https://docs.python.org/3/extending/index.html#extending-index

    本指南仅涵盖作为此版本 CPython 的一部分提供的用于创建扩展的基本工具。 Cython、cffi、SWIG 和 Numba 等第三方工具提供了更简单和更复杂的方法来为 Python 创建 C 和 C++ 扩展。

    从本质上讲,这条路线可能更多的是学术而非实用。话虽如此,我接下来要做的是,非常接近教程,创建一个模块文件。这本质上是 distutils 知道如何处理您的代码并从中创建 Python 模块的样板。在执行任何这些操作之前,最好先创建一个 Python 虚拟环境,这样您就不会污染您的系统包(参见 https://docs.python.org/3/library/venv.html#module-venv)。

    这是模块文件:

    // mt_np_forpy.cc
    //
    // C++ module implementation for multi-threaded min/max for np
    
    #define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION
    
    #include <python3.6/numpy/arrayobject.h>
    
    #include "mt_np.h"
    
    #include <cstdint>
    #include <iostream>
    
    using namespace std;
    
    /*
     * check:
     *  shape
     *  stride
     *  data_type
     *  byteorder
     *  alignment
     */
    static bool check_array(PyArrayObject *arr) {
      if (PyArray_NDIM(arr) != 1) {
        PyErr_SetString(PyExc_RuntimeError, "Wrong shape, require (1,n)");
        return false;
      }
      if (PyArray_STRIDES(arr)[0] != 8) {
        PyErr_SetString(PyExc_RuntimeError, "Expected stride of 8");
        return false;
      }
      PyArray_Descr *descr = PyArray_DESCR(arr);
      if (descr->type != NPY_LONGLTR && descr->type != NPY_DOUBLELTR) {
        PyErr_SetString(PyExc_RuntimeError, "Wrong type, require l or d");
        return false;
      }
      if (descr->byteorder != '=') {
        PyErr_SetString(PyExc_RuntimeError, "Expected native byteorder");
        return false;
      }
      if (descr->alignment != 8) {
        cerr << "alignment: " << descr->alignment << endl;
        PyErr_SetString(PyExc_RuntimeError, "Require proper alignement");
        return false;
      }
      return true;
    }
    
    template <typename T>
    static PyObject *mt_np_minmax_dispatch(PyArrayObject *arr) {
      npy_intp size = PyArray_SHAPE(arr)[0];
      T *begin = (T *)PyArray_DATA(arr);
      auto minmax =
          mt_np::min_max_mt(begin, begin + size, thread::hardware_concurrency());
      return Py_BuildValue("(L,L)", minmax.first, minmax.second);
    }
    
    static PyObject *mt_np_minmax(PyObject *self, PyObject *args) {
      PyArrayObject *arr;
      if (!PyArg_ParseTuple(args, "O", &arr))
        return NULL;
      if (!check_array(arr))
        return NULL;
      switch (PyArray_DESCR(arr)->type) {
      case NPY_LONGLTR: {
        return mt_np_minmax_dispatch<int64_t>(arr);
      } break;
      case NPY_DOUBLELTR: {
        return mt_np_minmax_dispatch<double>(arr);
      } break;
      default: {
        PyErr_SetString(PyExc_RuntimeError, "Unknown error");
        return NULL;
      }
      }
    }
    
    static PyObject *get_concurrency(PyObject *self, PyObject *args) {
      return Py_BuildValue("I", thread::hardware_concurrency());
    }
    
    static PyMethodDef mt_np_Methods[] = {
        {"mt_np_minmax", mt_np_minmax, METH_VARARGS, "multi-threaded np min/max"},
        {"get_concurrency", get_concurrency, METH_VARARGS,
         "retrieve thread::hardware_concurrency()"},
        {NULL, NULL, 0, NULL} /* sentinel */
    };
    
    static struct PyModuleDef mt_np_module = {PyModuleDef_HEAD_INIT, "mt_np", NULL,
                                              -1, mt_np_Methods};
    
    PyMODINIT_FUNC PyInit_mt_np() { return PyModule_Create(&mt_np_module); }
    

    在此文件中,大量使用了 Python 以及 NumPy API,有关更多信息,请参阅:https://docs.python.org/3/c-api/arg.html#c.PyArg_ParseTuple,对于 NumPy:https://docs.scipy.org/doc/numpy/reference/c-api.array.html

    安装模块

    接下来要做的是利用 distutils 安装模块。这需要一个设置文件:

    # setup.py
    
    from distutils.core import setup,Extension
    
    module = Extension('mt_np', sources = ['mt_np_module.cc'])
    
    setup (name = 'mt_np', 
           version = '1.0', 
           description = 'multi-threaded min/max for np arrays',
           ext_modules = [module])
    

    要最终安装模块,请在您的虚拟环境中执行python3 setup.py install

    测试模块

    最后,我们可以测试一下 C++ 实现是否真的优于 NumPy 的幼稚使用。为此,这里有一个简单的测试脚本:

    # timing.py
    # compare numpy min/max vs multi-threaded min/max
    
    import numpy as np
    import mt_np
    import timeit
    
    def normal_min_max(X):
      return (np.min(X),np.max(X))
    
    print(mt_np.get_concurrency())
    
    for ssize in np.logspace(3,8,6):
      size = int(ssize)
      print('********************')
      print('sample size:', size)
      print('********************')
      samples = np.random.normal(0,50,(2,size))
      for sample in samples:
        print('np:', timeit.timeit('normal_min_max(sample)',
                     globals=globals(),number=10))
        print('mt:', timeit.timeit('mt_np.mt_np_minmax(sample)',
                     globals=globals(),number=10))
    

    这是我做这一切的结果:

    8  
    ********************  
    sample size: 1000  
    ********************  
    np: 0.00012079699808964506  
    mt: 0.002468645994667895  
    np: 0.00011947099847020581  
    mt: 0.0020772050047526136  
    ********************  
    sample size: 10000  
    ********************  
    np: 0.00024697799381101504  
    mt: 0.002037393998762127  
    np: 0.0002713389985729009  
    mt: 0.0020942929986631498  
    ********************  
    sample size: 100000  
    ********************  
    np: 0.0007130410012905486  
    mt: 0.0019842900001094677  
    np: 0.0007540129954577424  
    mt: 0.0029724110063398257  
    ********************  
    sample size: 1000000  
    ********************  
    np: 0.0094779249993735  
    mt: 0.007134920000680722  
    np: 0.009129883001151029  
    mt: 0.012836456997320056  
    ********************  
    sample size: 10000000  
    ********************  
    np: 0.09471094200125663  
    mt: 0.0453535050037317  
    np: 0.09436299200024223  
    mt: 0.04188535599678289  
    ********************  
    sample size: 100000000  
    ********************  
    np: 0.9537652180006262  
    mt: 0.3957935369980987  
    np: 0.9624398809974082  
    mt: 0.4019058070043684  
    

    这些结果远没有线程前面显示的结果那么令人鼓舞,结果表明速度提高了大约 3.5 倍,并且没有包含多线程。我取得的结果在一定程度上是合理的,我预计线程的开销将占主导地位,直到数组变得非常大,此时性能提升将开始接近std::thread::hardware_concurrency x 增加。

    结论

    似乎对某些 NumPy 代码进行特定于应用程序的优化确实有空间,尤其是在多线程方面。我不清楚这是否值得努力,但它确实似乎是一个很好的练习(或其他东西)。我认为也许学习一些像 Cython 这样的“第三方工具”可能会更好地利用时间,但谁知道呢。

    【讨论】:

    • 我开始研究你的代码,知道一些 C++,但还没有使用 std::future 和 std::async。在您的“min_max_mt”模板函数中,它如何知道每个工作人员在触发和检索结果之间已经完成? (问只是为了理解,而不是说这有什么问题)
    • v = min_max_it-&gt;get();get 方法阻塞,直到结果准备好并返回它。由于循环会遍历每个未来,因此在它们全部完成之前它不会结束。 future.get()
    【解决方案11】:

    这是一个旧线程,但无论如何,如果有人再看这个......

    同时查找最小值和最大值时,可以减少比较次数。如果您正在比较的是浮点数(我猜是这样),这可能会为您节省一些时间,尽管不是计算复杂性。

    代替(Python代码):

    _max = ar[0]
    _min=  ar[0]
    for ii in xrange(len(ar)):
        if _max > ar[ii]: _max = ar[ii]
        if _min < ar[ii]: _min = ar[ii]
    

    您可以先比较数组中相邻的两个值,然后只比较较小的与当前最小值,较大的与当前最大值:

    ## for an even-sized array
    _max = ar[0]
    _min = ar[0]
    for ii in xrange(0, len(ar), 2)):  ## iterate over every other value in the array
        f1 = ar[ii]
        f2 = ar[ii+1]
        if (f1 < f2):
            if f1 < _min: _min = f1
            if f2 > _max: _max = f2
        else:
            if f2 < _min: _min = f2
            if f1 > _max: _max = f1
    

    这里的代码是用 Python 编写的,显然为了速度你会使用 C 或 Fortran 或 Cython,但这样你每次迭代进行 3 次比较,使用 len(ar)/2 次迭代,得到 3/2 * len(ar ) 比较。与此相反,以“显而易见的方式”进行比较,每次迭代进行两次比较,导致 2*len(ar) 比较。为您节省 25% 的比较时间。

    也许有一天有人会发现这很有用。

    【讨论】:

    • 您对此进行了基准测试吗?在现代 x86 硬件上,您有第一个变体中使用的 min 和 max 机器指令,这些避免了分支的需要,而您的代码放入了可能不会映射到硬件的控制依赖项。
    • 其实我没有。如果有机会我会做的。我认为很明显纯 python 代码将失去任何明智的编译实现,但我想知道在 Cython 中是否可以看到加速......
    • 在 numpy 中有一个 minmax 实现,在底层,由 np.bincount 使用,请参阅 here。它没有使用您指出的技巧,因为结果证明它比天真的方法慢了 2 倍。 PR 有一个链接到这两种方法的一些综合基准。
    【解决方案12】:

    我想出的最短方法是这样的:

    mn, mx = np.sort(ar)[[0, -1]]
    

    但是由于它对数组进行排序,所以效率不是最高的。

    另一个简短的方法是:

    mn, mx = np.percentile(ar, [0, 100])
    

    这样应该效率更高,但是计算结果,返回一个浮点数。

    【讨论】:

    • 可耻的是,这两个是本页中与其他解决方案相比最慢的解决方案:m = np.min(a); M = np.max(a) --> 0.54002 ||| m, M = f90_minmax1(a) --> 0.72134 ||| m, M = numba_minmax(a) --> 0.77323 ||| m, M = np.sort(a)[[0, -1]] --> 12.01456 ||| m, M = np.percentile(a, [0, 100]) --> 11.09418 ||| 100k 元素数组的 10000 次重复以秒为单位
    猜你喜欢
    • 2017-08-24
    • 1970-01-01
    • 2017-08-06
    • 2012-08-26
    • 2019-01-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-08-31
    相关资源
    最近更新 更多