Python 使用cython比struct.pack速度更快
我试图做得比struct.pack更好 以打包整数的特定案例为例,通过对的回答,我可以在Python 使用cython比struct.pack速度更快,python,struct,cython,pack,Python,Struct,Cython,Pack,我试图做得比struct.pack更好 以打包整数的特定案例为例,通过对的回答,我可以在pack_ints.pyx中打包一个整数列表: # cython: language_level=3, boundscheck=False import cython @cython.boundscheck(False) @cython.wraparound(False) def pack_ints(int_col): int_buf = bytearray(4*len(int_col))
pack_ints.pyx
中打包一个整数列表:
# cython: language_level=3, boundscheck=False
import cython
@cython.boundscheck(False)
@cython.wraparound(False)
def pack_ints(int_col):
int_buf = bytearray(4*len(int_col))
cdef int[::1] buf_view = memoryview(int_buf).cast('i')
idx: int = 0
for idx in range(len(int_col)):
buf_view[idx] = int_col[idx]
return int_buf
使用ipython中的此测试代码:
from struct import pack
import pyximport; pyximport.install(language_level=3)
import pack_ints
amount = 10**7
ints = list(range(amount))
res1 = pack(f'{amount}i', *ints)
res2 = pack_ints.pack_ints(ints)
assert(res1 == res2)
%timeit pack(f'{amount}i', *ints)
%timeit pack_ints.pack_ints(ints)
我得到:
304 ms ± 2.18 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
212 ms ± 6.54 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
我尝试将int\u buf
作为数组('b')
键入,但没有看到任何改进
是否有其他方法对此进行改进,或者以不同的方式使用cython,以使此操作更快?当我从原始问题运行代码时,我的速度提高了约5倍 当我在这里运行代码时,我看到您报告的结果,以及编译阶段的一个重要警告,我认为您忽略了这个警告:
警告:应键入pack_ints.pyx:13:17:索引以提高访问效率
我不确定它为什么没有正确地拾取类型,但要修复它,您应该将I
的定义更改回我最初编写的代码:
cdef int i
# not "i: int"
希望其他人会来尝试更聪明的方法,因为这显然有点荒谬。这个答案试图给出一个估计,一个并行版本可以产生多大的速度。但是,由于此任务是内存带宽受限的(Python integer对象至少占用32个字节,并且可以分散在内存中,因此会有许多缓存未命中),因此我们不应该期望太多 第一个问题是,如何处理错误(元素不是整数或值太大)。我将遵循以下策略/简化:当对象
- 不是整数
- 是负整数
- 或整数>=2^30
-1
),表示出了问题。仅允许非负整数2**30-1
int到_int(PyObject*vv){
如果(PyLong_检查(vv)){
PyLongObject*v=(PyLongObject*)vv;
Py_ssize_t i=Py_尺寸(v);
如果(i==0){
返回0;
}
如果(i==1){//一个数字足够小
返回v->ob_位[0];
}
//负(i1)
返回-1;
}
返回-1;
}
现在给定一个列表,我们可以将其转换为一个int
-缓冲区,并使用以下使用omp的C函数:
void convert_list(PyListObject *lst, int *output){
Py_ssize_t n = Py_SIZE(lst);
PyObject **data = lst->ob_item;
#pragma omp parallel for
for(Py_ssize_t i=0; i<n; ++i){
output[i] = to_int(data[i]);
}
}
一个重要的细节是:convert\u list
不能是nogil(因为它不是)!Omp线程和Python线程(受GIL影响)完全不同
在使用对象时,可以(但没有必要)为omp操作释放GIL,因为这些对象通过缓冲协议被锁定,不能从不同的Python线程中更改。list
没有这样的锁定机制,因此,如果GIL被释放,列表可能会在另一个线程中被更改,我们所有的指针都可能失效
现在来看看时间安排(列表略大):
金额=5*10**7
整数=列表(范围(金额))
%timeit包(f'{amount}i',*整数)
#每个回路1.51 s±38.9 ms(7次运行的平均值±标准偏差,每个回路1次)
%timeit pack\u ints\u DavidW(ints)
#每个回路284 ms±3.1 ms(7次运行的平均值±标准偏差,每个回路1次)
%timeit pack_ints_ead(ints)
#每个回路177 ms±11.8 ms(7次运行的平均值±标准偏差,每个10个回路)
顺便说一句,关闭pack\u ints\u ead的并行化会导致运行时间为209ms
因此,考虑到约33%的适度改善,我会选择更稳健的DavidW解决方案
以下是一种稍有不同的方法来表示错误值的实现:
- 不是整数对象会导致
-2147483648
(即0x8000000
)-32位int可以存储的最小负值
- 整数
=2147483647
(即=0x7fffffff
)将映射到/存储为2147483647
——32位整数可以存储的最大正数
- 整数
>1)==0){
int add=(v->ob_位[1]&1)ob_位[0]+add);
}
返回符号*0x7fffffff;
}
返回0x8000000;
}
我怀疑你能跑得这么快。为什么您认为有可能更快地执行操作?例如,我猜如果有人通过一些C代码获得指向列表对象的指针,他可以打开线程并并行工作,速度会随着内核数量的增加而加快。我不知道如何在cython中实现这一点,在这个函数上无法访问@nogil
。我认为您需要gil将python对象转换为c-int(例如,它可能引发异常)。我建议您做一件更有用的事情,就是修复输入数据。您是否可以直接生成打包数据,而不是浪费时间生成一个大型Python对象列表,然后对其进行转换?。。。还有。您需要调整实现,使其不使用GIL(即不能引发异常)。谢谢!这段C代码是由cython编译的吗?此外,如果我使用tuple()
将列表转换为一个元组,那么我可以使用nogil
?@Jay将C代码合并到Cython同时生成和编译的C文件中。转换为tuple
对nogil
没有影响,但是nogil
在这里并不重要-循环会像您希望的那样并行运行(没有一个线程需要GIL,因为它们只是读取而不是更改,但是其中一个线程应该有GIL,这样就没有其他线程可以并行运行并更改东西了-我同意这有点混乱…@Jay我使用它来包含C代码逐字记录。您可以对实现缓冲协议的对象使用nogil()元组和列表都没有。但是,正如DavidW指出的那样,您不需要这样做-计算已经并行进行。我相信在检查注释时这行是白色的,所以我不关心警告。如果cdef跳到X5@Jay改变
void convert_list(PyListObject *lst, int *output){
Py_ssize_t n = Py_SIZE(lst);
PyObject **data = lst->ob_item;
#pragma omp parallel for
for(Py_ssize_t i=0; i<n; ++i){
output[i] = to_int(data[i]);
}
}