threadpoolexecutor结合cython的nogil使用
Usage of threadpoolexecutor in conjunction with cython's nogil
我已经阅读了这个问题和答案 - 我有一个类似的问题,我的 Cython 代码没有得到预期的加速,尽管我的系统有多个内核。我在 Ubuntu 18.04 实例上有 4 个物理内核,如果我在下面的代码中将作业数设置为 1,它的运行速度会比我设置为 4 时快。使用 top 查看 CPU 用法我看到 CPU 使用率高达 300%。我正在 C++ class 中查找未修改的数据结构,即我仅通过 Cython 对 C++ 数据结构进行只读查询。 C++ 端没有任何互斥锁。
这是我第一次接触GIL,我想知道我是不是用错了。此外,时间的输出有点令人困惑,因为我认为它没有正确描述每个工作线程所花费的实际时间。
我似乎错过了一些重要的东西,但我无法弄清楚它是什么,因为我几乎使用了相同的模板来使用 GIL,如链接的 SO 答案中所示。
import psutil
import numpy as np
from concurrent.futures import ThreadPoolExecutor
from functools import partial
cdef extern from "Rectangle.h" namespace "shapes":
cdef cppclass Rectangle:
Rectangle(int, int, int, int)
int x0, y0, x1, y1
int getArea() nogil
cdef class PyRectangle:
cdef Rectangle *rect
def __cinit__(self, int x0, int y0, int x1, int y1):
self.rect = new Rectangle(x0, y0, x1, y1)
def __dealloc__(self):
del self.rect
def testThread(self):
latGrid = np.arange(minLat,maxLat,0.05)
lonGrid = np.arange(minLon,maxLon,0.05)
gridLon,gridLat = np.meshgrid(latGrid,lonGrid)
grid_points = np.c_[gridLon.ravel(),gridLat.ravel()]
n_jobs = psutil.cpu_count(logical=False)
chunk = np.array_split(grid_points,n_jobs,axis=0)
x = ThreadPoolExecutor(max_workers=n_jobs)
t0 = time.time()
func = partial(self.performCalc,maxDistance)
results = x.map(func,chunk)
results = np.vstack(list(results))
t1 = time.time()
print(t1-t0)
def performCalc(self,maxDistance,chunk):
cdef int area
cdef double[:,:] gPoints
gPoints = memoryview(chunk)
for i in range(0,len(gPoints)):
with nogil:
area = self.getArea2(gPoints[i])
return area
cdef int getArea2(self,double[:] p) nogil :
cdef int area
area = self.rect.getArea()
return area
我的建议(在评论中)是确保整个 performCalc
循环是 nogil
。为此,需要进行一些更改:
cdef Py_ssize_t i # set type of "i" (although Cython can possibly deduce this anyway)
with nogil:
for i in range(0,gPoints.shape[0]):
area = self.getArea2(gPoints[i])
其中最重要的是将 len(gPoints)
替换为 gPoints.shape[0]
,它用数组查找替换了对 Python 函数的调用(我个人也不认为 len
对二维数组有意义)。
从本质上讲,获取和发布 GIL 是有成本的。您想要确保在没有 GIL 的情况下完成的工作值得花时间处理它。简单地计算一个矩形的面积是非常微不足道的(两次减法和一次乘法),因此并不能真正证明协调线程之间的 GIL 所花费的时间是合理的 - 请记住,每个循环一次,每个线程必须(短暂地)持有 GIL,在此期间时间没有其他线程可以容纳它。然而,随着整个循环 nogil
花在管理它上的时间变得很少。
我已经阅读了这个问题和答案 -
这是我第一次接触GIL,我想知道我是不是用错了。此外,时间的输出有点令人困惑,因为我认为它没有正确描述每个工作线程所花费的实际时间。
我似乎错过了一些重要的东西,但我无法弄清楚它是什么,因为我几乎使用了相同的模板来使用 GIL,如链接的 SO 答案中所示。
import psutil
import numpy as np
from concurrent.futures import ThreadPoolExecutor
from functools import partial
cdef extern from "Rectangle.h" namespace "shapes":
cdef cppclass Rectangle:
Rectangle(int, int, int, int)
int x0, y0, x1, y1
int getArea() nogil
cdef class PyRectangle:
cdef Rectangle *rect
def __cinit__(self, int x0, int y0, int x1, int y1):
self.rect = new Rectangle(x0, y0, x1, y1)
def __dealloc__(self):
del self.rect
def testThread(self):
latGrid = np.arange(minLat,maxLat,0.05)
lonGrid = np.arange(minLon,maxLon,0.05)
gridLon,gridLat = np.meshgrid(latGrid,lonGrid)
grid_points = np.c_[gridLon.ravel(),gridLat.ravel()]
n_jobs = psutil.cpu_count(logical=False)
chunk = np.array_split(grid_points,n_jobs,axis=0)
x = ThreadPoolExecutor(max_workers=n_jobs)
t0 = time.time()
func = partial(self.performCalc,maxDistance)
results = x.map(func,chunk)
results = np.vstack(list(results))
t1 = time.time()
print(t1-t0)
def performCalc(self,maxDistance,chunk):
cdef int area
cdef double[:,:] gPoints
gPoints = memoryview(chunk)
for i in range(0,len(gPoints)):
with nogil:
area = self.getArea2(gPoints[i])
return area
cdef int getArea2(self,double[:] p) nogil :
cdef int area
area = self.rect.getArea()
return area
我的建议(在评论中)是确保整个 performCalc
循环是 nogil
。为此,需要进行一些更改:
cdef Py_ssize_t i # set type of "i" (although Cython can possibly deduce this anyway)
with nogil:
for i in range(0,gPoints.shape[0]):
area = self.getArea2(gPoints[i])
其中最重要的是将 len(gPoints)
替换为 gPoints.shape[0]
,它用数组查找替换了对 Python 函数的调用(我个人也不认为 len
对二维数组有意义)。
从本质上讲,获取和发布 GIL 是有成本的。您想要确保在没有 GIL 的情况下完成的工作值得花时间处理它。简单地计算一个矩形的面积是非常微不足道的(两次减法和一次乘法),因此并不能真正证明协调线程之间的 GIL 所花费的时间是合理的 - 请记住,每个循环一次,每个线程必须(短暂地)持有 GIL,在此期间时间没有其他线程可以容纳它。然而,随着整个循环 nogil
花在管理它上的时间变得很少。