Cython在boundscheck=True的情况下比boundscheck=False更快。

13
考虑以下简单示例:
#cython: language_level=3, boundscheck=False, wraparound=False, initializedcheck=False, cdivision=True
cimport cython
from libc.stdlib cimport malloc

def main(size_t ni, size_t nt, size_t nx):
    cdef:
        size_t i, j, t, x, y
        double[:, :, ::1] a = <double[:ni, :ni, :nx]>malloc(ni * ni * nx * sizeof(double))
        double[:, :, ::1] b = <double[:nt, :ni, :nx]>malloc(nt * ni * nx * sizeof(double))
        size_t[:, :, ::1] best = <size_t[:nt, :ni, :nx]>malloc(nt * ni * nx * sizeof(size_t))
        size_t mxi
        double s, mxs
    for t in range(nt):
        for j in range(ni):
            for y in range(nx): # this loops does nothing but is needed for the effect below.
                mxs = -1e300
                for i in range(ni):
                    for x in range(nx):
                        with cython.boundscheck(False): # Faster!?!?
                            s = b[t, i, x] + a[i, j, x]
                        if s >= mxs:
                            mxs = s
                            mxi = i
                best[t + 1, j, y] = mxi
    return best[0, 0, 0]

基本上是将两个二维数组沿着特定的轴求和,并找到另一个轴上的最大索引。
使用gcc -O3编译并使用参数(1, 2000, 2000)调用时,添加boundscheck=True会导致执行速度比boundscheck=False快两倍。
为什么会出现这种情况的任何提示?(嗯,我可以猜测这可能再次与GCC自动向量化有关...)

1
在我的测试中,使用 with cython.boundscheck(True) 的版本大约慢了3倍。我认为由于malloc的原因,abbest的内存都未初始化。我将其更改为等效的calloc调用。此外,当t == nt - 1时,行best[t + 1, j, y]似乎正在索引无效的内存。 - Alok--
将malloc更改为calloc并将“t + 1”替换为“t”对我来说在质量上没有改变结果。我的setup.py具有extra_compile_args=["-O3"],我正在使用gcc 5.1.0。 - antony
1
好的,我尝试了-O3,两个版本的速度都一样。我正在使用gcc 4.9.1。看着生成的C代码,我认为gcc足够聪明,知道额外的边界检查永远不会被触发(因为for循环条件)。但我不知道你为什么会得到如此不同的速度。 - Alok--
我刚刚在另一台机器上使用gcc 4.9.2进行了测试,仍然得到类似的结果,尽管现在差异只有约50%。 - antony
2
你最终解决了这个问题吗? - Jay
1
不,我主要转向使用pybind11了。 - antony
1个回答

0

Boundscheck是一种安全检查,用于确保您访问向量内的索引在边界范围内。如果您不进行此检查并且索引可能超出边界,则速度会更快,因为执行检查需要时间。

也就是说,如果boundscheck为true,它将在读取或写入内存之前检查索引是否在向量范围内。如果不在范围内,它将抛出错误。如果boundscheck为false,则将读取或写入指针,即使索引超出了边界,也会给出错误数据并通过写入来破坏数据。

来自文档的说明如下:

数组查找仍然受到两个因素的影响:

1)执行边界检查。

2)检查负索引并正确处理。

不进行边界检查的后果是:

现在不会执行边界检查(并且,作为副作用,如果您“确实”访问超出边界,则在最好的情况下会使程序崩溃,在最坏的情况下会破坏数据)。

这在您可以有“无”的向量时尤其重要。以下是文档中的警告:

警告

速度是有代价的。特别是将类型化对象(例如我们示例代码中的 f、g 和 h)设置为 None 可能很危险。将此类对象设置为 None 是完全合法的,但您只能检查它们是否为 None。所有其他用途(属性查找或索引)都可能导致段错误或损坏数据(而不是像在 Python 中引发异常)。

实际规则有点更复杂,但主要信息很清楚:不要在不知道它们未被设置为 None 的情况下使用类型化对象。

http://docs.cython.org/src/userguide/numpy_tutorial.html


8
我认为提出这个问题的原因是,在这种情况下,boundscheck(True) 更快,这是违反直觉的。 - DavidW
1
哦!!!好观点!这么违反直觉,我甚至把问题读错了!我以为问题问的是相反的。 - patapouf_ai

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