我需要在Cython中使用`nogil`吗?

8

我有一些Cython代码,希望能以最快的速度运行。为了实现这个目标,我需要释放GIL吗?

假设我的代码类似于以下示例:

import numpy as np

# trivial definition just for illustration!
cdef double some_complicated_function(double x) nogil:
    return x

cdef void func(double[:] input) nogil:
    cdef double[:] array = np.zeros_like(input)
    for i in range(array.shape[0]):
        array[i] = some_complicated_function(input[i])

我从 np.zeros_like 这一行代码中得到了一堆类似于以下的错误消息:

nogilcode.pyx:7:40: Calling gil-requiring function not allowed without gil
nogilcode.pyx:7:29: Accessing Python attribute not allowed without gil
nogilcode.pyx:7:27: Accessing Python global or builtin not allowed without gil
nogilcode.pyx:7:40: Constructing Python tuple not allowed without gil
nogilcode.pyx:7:41: Converting to Python object not allowed without gil

我需要找到一种不需要GIL的调用 np.zeros_like 或其他方式来分配数组的方法吗?

你可以在Cython中使用NumPy的C-API,相比于调用Python的zeros_like函数,这样做可以稍微提高一些速度。由于你填充了每个值,所以可以创建一个empty数组而不是一个zeros数组。 - MaxNoe
1
好的观点,这可能会有所帮助。我应该说,这个问题更多地涉及到在Cython中何时使用nogil比np.zeros_like具体。许多人似乎从一开始就认为所有东西都需要nogil,而没有真正的理由,我想写一个好的答案来参考他们。 - DavidW
我有同样的问题,即在cdef函数内使用np.sum(a_mem_view)可能不会释放GIL。DavidW,你真是一个英雄,解决了这么多cython问题,干得好! - avocado
1个回答

10

不需要 - 你可能不需要释放GIL。

GIL(全局解释器锁)的基本功能是确保Python的内部机制不受竞争条件的影响,通过确保一次只有一个Python线程能够运行。然而,仅仅持有GIL并不会减慢您的代码速度。

当两个(相关的)情况发生时,您应该释放GIL:

  1. 使用Cython的并行机制。例如,一个prange循环的内容需要被要求为nogil

  2. 如果您希望其他(外部)Python线程能够同时运行。

    a. 如果您有一个不需要GIL的大型计算/IO密集型块,则“礼貌”地释放它可能有利于想要进行多线程的用户,但这主要是有用而非必要的。

    b. (非常、非常偶尔)有时候使用短的with nogil: pass块暂时释放GIL是有用的。这是因为Cython不会自动释放它(与Python不同),因此如果您正在等待另一个Python线程完成任务,则可以避免死锁。除非您正在使用Cython编译GUI代码,否则这个子点可能不适用于您。


能够在没有GIL的情况下运行的Cython代码(没有调用Python,纯粹的C级别数值操作)通常是高效运行的代码。有时候这会让人们产生误解,认为关键在于释放GIL,而不是他们实际运行的代码。不要被这个误导 - 您的(单线程)代码将在有或没有GIL的情况下以相同的速度运行。

因此,如果您有一个很好的快速Numpy函数可以快速地处理大量数据,但只能在GIL的情况下调用,那么就直接调用它 - 不会造成任何伤害!


作为最后一点:即使在nogil块内(例如prange循环),如果需要,您仍然可以重新获取GIL:
with gil:
    ... # small block of GIL requiring code goes here

尽量不要经常这样做(获取/释放需要时间,当然只能有一个线程在运行此块),但同样这是在需要时进行小型Python操作的好方法。

有时候人们会误解,认为关键在于释放GIL,而不是他们实际运行的代码。你对这个说法有多有信心?我记得我曾经实现过树结构,在某些情况下,将最基本的函数调用转换为“nogil”可以显著加速我的代码,而我并没有进行任何并行或线程处理。可能我记错了,因为那已经是一段时间以前的事情了。 - oli
@Oli 我很难想到一个机制,它能够产生很大的差异,所以我“相当有信心”。然而,我以前也曾经错过一些事情!如果你有反例,我会很感兴趣看看。 - DavidW
嗨DavidW,感谢您提供如此详细的答案!我有一个愚蠢的问题,使用Numpy函数的Cython代码似乎并不比纯Py Numpy版本更快。我的用例是在Cython中使用Numpy函数实现一些ML算法,并且我想使用Numpy广播函数(例如np.lognp.expnp.max),因为它们非常方便使用。通过%timeit,Cython版本几乎与Py版本相同,这是否符合预期? - avocado
1
@avocado 是的,这是可以预料的。Cython 无法查看 Numpy 函数内部以加速它们(它们通常在内部非常快速,因此几乎没有什么可获得的)。 - DavidW
1
@oli:我基本上可以确定,单线程的Python程序使用nogil不会有任何好处。当GIL没有争用时,它基本上是免费的(这就是为什么他们使用了GIL;它是实现安全线程最简单的方式,而且不会对单线程程序造成惩罚)。如果您是单线程的,释放和重新获取GIL的成本非常微不足道(在大多数操作系统上,获取/释放未争用锁几乎是免费的),因此在没有线程的情况下使用nogil是无害的,但只会通过偶然的方式加速代码(更好地对齐缓存)。 - ShadowRanger

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接