高效的 cython 文件读取、字符串解析和数组构建

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。(移动滑块 > 运行背景分析 > 加载数据 > 实时绘制结果)。

HYR*_*YRY 5

这是一个更快的示例,它用于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)