flu*_*ak7 8 python numpy cython
所以我有一些看起来像这样的数据文件:
47
425 425 -3 15000 15000 900 385 315 3 370 330 2 340 330 2
325 315 2 325 240 2 340 225 2 370 225 2 385 240 2
385 315 2 475 240 3 460 240 2 460 255 2 475 255 2
475 240 2 595 315 3 580 330 2 550 330 2 535 315 2
535 240 2 550 225 2 580 225 2 595 240 2 595 315 2
700 315 3 685 330 2 655 330 2 640 315 2 640 240 2
655 225 2 685 225 2 700 240 2 700 315 2 700 315 3
9076 456 2 9102 449 2 9127 443 2 9152 437 2 9178 433 2
9203 430 2 9229 428 2 9254 427 2 9280 425 2 9305 425 2
0 0 999 6865 259999
20
425 425 -3 15000 15000 900 385 315 3 370 330 2 340 330 2
325 315 2 325 240 2 340 225 2 370 225 2 385 240 2
385 315 2 475 240 3 460 240 2 460 255 2 475 255 2
475 240 2 595 315 3 580 330 2 550 330 2 535 315 2
Run Code Online (Sandbox Code Playgroud)
第一个数字是后面文本块中的点数,然后文本块有那么多点,每行最多 5 个点。每个点有 3 个分量(我将它们称为 x、y、z)。x 和 y 得到 6 个字符,而 z 得到 4 个字符,因此每个点需要 16 个字符。有时 z 是 9999,导致 y 和 z 之间没有空格,因此使用 split() 会弄乱解析这些行。此外,所有数字都是整数(没有小数),但也有一些负数。
在实际文件中,块通常为 1000 点长,有些块更小(在“页面”的末尾,其中分页符由 z=9999 表示)
我最初的解决方案是使用正则表达式:
import re
def get_points_regex(filename):
with open(filename, 'r') as f:
text = f.read()
points = []
for m in re.finditer('([ \d-]{6})([ \d-]{6})([ \d\-]{4})', text):
point = tuple(int(i) for i in m.groups())
points.append(point)
return points
Run Code Online (Sandbox Code Playgroud)
我的测试文件长 55283 行(4.4 MB),包含 274761 个点。
使用 timeitget_points_regex我得到 560 毫秒。
然后我发现虽然 finditer 具有内存效率,但当我不需要它们的任何功能时,生成数千个匹配对象的速度很慢,所以我使用re.findall以下方法制作了一个版本:
def get_points_regex2():
with open(filename, 'r') as f:
text = f.read()
points = re.findall(r'([ \d-]{6})([ \d-]{6})([ \d\-]{4})', text)
points = [tuple(map(int, point)) for point in points]
return points
Run Code Online (Sandbox Code Playgroud)
此版本的运行时间为 414 毫秒,比 Finditer 快 1.35 倍。
然后我在想,对于这样简单的模式,正则表达式可能有点矫枉过正,所以我使用纯 python 制作了一个版本:
def get_points_simple():
points = []
with open(filename, 'r') as f:
for line in f:
n_chunks = int(len(line)/16)
for i in range(n_chunks):
chunk = line[16*i:16*(i+1)]
x = int(chunk[0:6])
y = int(chunk[6:12])
z = int(chunk[12:16])
points.append((x, y, z))
return points
Run Code Online (Sandbox Code Playgroud)
这在 386 毫秒内运行,比正则表达式快 1.07 倍。
然后我崩溃了,第一次尝试了 Cython。我只是%%cython在 jupyter notebook 中使用cell magic运行。我想出了这个:
%%cython
def get_points_cython(filename):
cdef int i, x, y, z
points = []
f = open(filename, 'r')
for line in f:
n_chunks = int(len(line)/16)
for i in range(n_chunks):
chunk = line[16*i:16*(i+1)]
x = int(chunk[0:6])
y = int(chunk[6:12])
z = int(chunk[12:16])
points.append((x, y, z))
f.close()
return points
Run Code Online (Sandbox Code Playgroud)
cython 函数在 196 毫秒内运行。(比纯 python 快 2 倍)
我试图简化一些表达式,比如不使用上下文管理器打开文件。虽然我声明了整数,但我不确定还能做什么,所以我不理会剩下的。我做了几次尝试做一个 2D 整数数组而不是一个元组列表points,但是 python segfaulted(我假设这就是发生的事情,IPython 内核死了)。我曾cdef int points[1000000][3]那么我标注相同的语句points[j][1] = x,而递增j。从一些轻松的阅读和很少的 C 背景来看,我认为这可能是一个相当大的数组?堆栈与堆(我不知道这些到底是什么)?需要 malloc 之类的东西吗?我有点迷失在那些东西上。
接下来我读到,也许我应该只使用 Numpy,因为 Cython 擅长于此。在此之后,我能够创建此功能:
%%cython
import numpy as np
cimport numpy as np
DTYPE = np.int
ctypedef np.int_t DTYPE_t
def get_points_cython_numpy(filename):
cdef int i, j, x, y, z
cdef np.ndarray points = np.zeros([1000000, 3], dtype=DTYPE)
f = open(filename, 'r')
j = 0
for line in f:
n_chunks = int(len(line)/16)
for i in range(n_chunks):
chunk = line[16*i:16*(i+1)]
x = int(chunk[0:6])
y = int(chunk[6:12])
z = int(chunk[12:16])
points[j, 0] = x
points[j, 1] = y
points[j, 2] = z
j = j + 1
f.close()
return points
Run Code Online (Sandbox Code Playgroud)
不幸的是,这需要 263 毫秒,所以会慢一点。
我是否遗漏了一些明显的 cython 或 python std lib 可以使解析速度更快,或者对于这种大小的文件,这是否与它获得的速度一样快?
我考虑过 pandas 和 numpy 加载函数,但我认为块大小的行会使它变得过于复杂。有一次,我对 Pandas read_fwf 进行了一些工作,然后是 DataFrame.values.reshape(-1, 3),然后用 NaN 删除行,但我知道到那时必须更慢。
任何加快速度的想法将不胜感激!
我很想将其降低到 100 毫秒以下,以便在生成这些文件时可以通过读取这些文件来快速更新 GUI。(移动滑块 > 运行背景分析 > 加载数据 > 实时绘制结果)。
这是一个更快的示例,它用于fast_atoi()将字符串转换为 int,比get_points_cython()在我的电脑上快 2 倍。如果点数线具有相同的宽度(8 个字符),那么我想我可以进一步加速它(大约快 12 倍get_points_cython())。
%%cython
import numpy as np
cimport numpy as np
import cython
cdef int fast_atoi(char *buff):
cdef int c = 0, sign = 0, x = 0
cdef char *p = buff
while True:
c = p[0]
if c == 0:
break
if c == 45:
sign = 1
elif c > 47 and c < 58:
x = x * 10 + c - 48
p += 1
return -x if sign else x
@cython.boundscheck(False)
@cython.wraparound(False)
def get_points_cython_numpy(filename):
cdef int i, j, x, y, z, n_chunks
cdef bytes line, chunk
cdef int[:, ::1] points = np.zeros([500000, 3], np.int32)
f = open(filename, 'rb')
j = 0
for line in f:
n_chunks = int(len(line)/16)
for i in range(n_chunks):
chunk = line[16*i:16*(i+1)]
x = fast_atoi(chunk[0:6])
y = fast_atoi(chunk[6:12])
z = fast_atoi(chunk[12:16])
points[j, 0] = x
points[j, 1] = y
points[j, 2] = z
j = j + 1
f.close()
return points.base[:j]
Run Code Online (Sandbox Code Playgroud)
这是最快的方法,想法是将整个文件内容读入一个字节对象,并从中获取点数据。
@cython.boundscheck(False)
@cython.wraparound(False)
cdef inline int fast_atoi(char *buf, int size):
cdef int i=0 ,c = 0, sign = 0, x = 0
for i in range(size):
c = buf[i]
if c == 0:
break
if c == 45:
sign = 1
elif c > 47 and c < 58:
x = x * 10 + c - 48
return -x if sign else x
@cython.boundscheck(False)
@cython.wraparound(False)
def fastest_read_points(fn):
cdef bytes buf
with open(fn, "rb") as f:
buf = f.read().replace(b"\n", b"") # change it with your endline.
cdef char * p = buf
cdef int length = len(buf)
cdef char * buf_end = p + length
cdef int count = length // 16 * 2 # create enough large array
cdef int[:, ::1] res = np.zeros((count, 3), np.int32)
cdef int i, j, block_count
i = 0
while p < buf_end:
block_count = fast_atoi(p, 10)
p += 10
for j in range(block_count):
res[i, 0] = fast_atoi(p, 6)
res[i, 1] = fast_atoi(p+6, 6)
res[i, 2] = fast_atoi(p+12, 4)
p += 16
i += 1
return res.base[:i]
Run Code Online (Sandbox Code Playgroud)