Cython:为什么size_t比int更快?

9

将某些Cython变量从类型int更改为类型size_t可以显著减少某些函数的运行时间(约30%),但我不知道为什么。

例如:

cimport numpy as cnp
import numpy as np

def sum_int(cnp.int64_t[::1] A):
    cdef unsigned long s = 0
    cdef int k
    for k in xrange(A.shape[0]):
        s += A[k]
    return s

def sum_size_t(cnp.int64_t[::1] A):
    cdef unsigned long s = 0
    cdef size_t k
    for k in xrange(A.shape[0]):
        s += A[k]
    return s

a = np.array(range(1000000))

接下来是时间结果:

In [17]: %timeit sum_int(a)   
1000 loops, best of 3: 652 µs per loop

In [18]: %timeit sum_size_t(a)
1000 loops, best of 3: 427 µs per loop

我刚接触Cython,但是比起C语言更熟悉Fortran。请帮我解答一下,这两种变量类型之间的重要区别导致了如此大的性能差异?我对Cython还有哪些方面不太了解呢?


你检查过生成的汇编代码了吗? - Karoly Horvath
@KarolyHorvath 我试着看生成的C代码,但几乎快瞎了眼。我相信它是高度优化的,但作为一个Fortran人员,自动生成的C代码相当难以阅读。 - john_science
你正在使用一个 int64_t 数组。为什么不在累加器中使用 int64_t 呢? - user2357112
@user2357112 哈哈。我猜我认为添加一百万个数字可能会导致 64 位整数溢出。粗心。但是,这不会改变结果。累加器类型在这两个示例之间不会改变。 - john_science
1
size_t不是隐式无符号的吗?由于您在循环中使用变量k,我猜如果您使用size_t或无符号整数,它会被优化,因为Cython文档报告说“当索引值已由cdef声明时,例如,'range()'是C优化的”。所以,您后面用作A索引器的无符号值永远不会为负(并且它应该允许您将wraparoundboundscheck参数设置为False,因为您正在安全地循环遍历数组的边界,并且可能具有更好的性能)? - mgc
@mgc 哦!我刚刚测试了一下。那真的很接近了。我再试了一次,使用 unsigned int 代替 int,几乎接近于 size_t 的速度。不过仍然只有大约80%的速度。如此接近! - john_science
2个回答

15

你可能需要逐行分析才能找出确切的答案,但生成的C文件中有一件事引起了我的注意:int版本被检查是否会变为负数,而size_t则被认为是可行的。

在int循环中:(t_3k赋值,它们是相同类型)

if (__pyx_t_3 < 0) {
  __pyx_t_3 += __pyx_v_A.shape[0];
  if (unlikely(__pyx_t_3 < 0)) __pyx_t_4 = 0;
} else if (unlikely(__pyx_t_3 >= __pyx_v_A.shape[0])) __pyx_t_4 = 0;
在 size_t 循环中:
if (unlikely(__pyx_t_3 >= (size_t)__pyx_v_A.shape[0])) __pyx_t_4 = 0;
因为size_t是无符号的,在内存中索引项目时无需进行回绕测试,因此不需要进行回绕测试。其余部分基本相同。
更新: 关于您的unsigned int结果 - 您的 int 和 size_t 的大小是否不同?有可能导致更改吗?在我的情况下,uint 和 size_t 的 C 代码是相同的。(因为在这个系统上,size_t 是无符号的,具体为 unsigned int)

1
好的,我进行了一些时间测试,并发现如果我使用“unsigned int”并关闭装饰器的环绕测试,就像你上面建议的那样,我可以获得“size_t”的性能。因此,如果您添加一个注释说明“size_t”是无符号的,我将接受您的答案。 - john_science
2
一旦你知道自己在做什么,就要始终关闭边界检查和环绕。我不知道使用size_t会自动暗示这一点。 - dividebyzero

5
在64位系统上,似乎有两个原因:
  1. Use an unsigned integer for the loop:

    %%cython
    
    cimport numpy as cnp
    import numpy as np
    
    def sum_int_unsigned(cnp.int64_t[::1] A):
        cdef unsigned long s = 0
        cdef unsigned k
        for k in xrange(A.shape[0]):
            s += A[k]
        return s
    
  2. Use a long instead of an int:

    %%cython
    
    cimport numpy as cnp
    import numpy as np
    
    def sum_int_unsigned_long(cnp.int64_t[::1] A):
        cdef unsigned long s = 0
        cdef unsigned long k
        for k in xrange(A.shape[0]):
            s += A[k]
        return s
    

时间:

%timeit sum_int(a)
1000 loops, best of 3: 1.52 ms per loop

%timeit sum_size_t(a)
1000 loops, best of 3: 671 µs per loop

使用无符号类型可以解决一半的问题:
%timeit sum_int_unsigned(a) 
1000 loops, best of 3: 1.09 ms per loop

使用 long 类型来处理剩余部分:

%timeit sum_int_unsigned_long(a)
1000 loops, best of 3: 648 µs per loop

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