TL;DR: 这是由于
numpy.random.multinomial
函数中的
额外检查 开销导致的
本地性能回归。由于所需检查的相对执行时间,
非常小的数组 受到强烈影响。
底层实现
在Numpy代码的Git提交记录中进行二分查找,发现性能回归最初出现在2019年4月中旬。可以在提交dd77ce3cb
中重现,但不能在7e8e19f9a
中。其中有一些构建问题,但是通过一些快速修复,我们可以证明提交0f3dd0650
是第一个导致问题的提交。该提交说明了它:
扩展多项式以允许广播
修复在NumPy中遗漏的zipf更改
将0作为超几何的有效输入启用
深入分析此提交发现它修改了Cython文件mtrand.pyx
中定义的multinomial
函数,执行以下两个额外的检查:
def multinomial(self, np.npy_intp n, object pvals, size=None):
cdef np.npy_intp d, i, sz, offset
cdef np.ndarray parr, mnarr
cdef double *pix
cdef int64_t *mnix
cdef int64_t ni
d = len(pvals)
parr = <np.ndarray>np.PyArray_FROM_OTF(pvals, np.NPY_DOUBLE, np.NPY_ALIGNED)
pix = <double*>np.PyArray_DATA(parr)
check_array_constraint(parr, 'pvals', CONS_BOUNDED_0_1)
if kahan_sum(pix, d-1) > (1.0 + 1e-12):
raise ValueError("sum(pvals[:-1]) > 1.0")
if size is None:
shape = (d,)
else:
try:
shape = (operator.index(size), d)
except:
shape = tuple(size) + (d,)
multin = np.zeros(shape, dtype=np.int64)
mnarr = <np.ndarray>multin
mnix = <int64_t*>np.PyArray_DATA(mnarr)
sz = np.PyArray_SIZE(mnarr)
ni = n
check_constraint(ni, 'n', CONS_NON_NEGATIVE)
offset = 0
with self.lock, nogil:
for i in range(sz // d):
random_multinomial(self._brng, ni, &mnix[offset], pix, d, self._binomial)
offset += d
return multin
这两个检查对于代码的
稳健性是必需的。然而,考虑到它们的目的,它们目前相当
昂贵。
实际上,在我的机器上,第一个检查占据了总运行时间的约75%,第二个检查占据了约20%。尽管这些检查只需要几微秒的时间,但由于输入非常小,因此开销与计算时间相比非常巨大。
解决此问题的一个方法是编写一个特定的Numba函数,因为您的输入数组非常小。在我的机器上,在一个简单的Numba函数中使用np.random.multinomial
可以获得良好的性能。