cython:减少class的大小,减少内存使用,提高速度

cython: reducing the size of a class, reduce memory use, improve speed

我有一个相对简单的问题:给定基因组中的一个位置,return 那个位置的基因名称。

我现在解决这个问题的方法是在 cython::

中使用以下 class
class BedFile():
    """ A Lookup Object """
    def __init__(self, bedfile):
        self.dict = {}
        cdef int start, end
        with open(bedfile) as infile:
            for line in infile:
                f = line.rstrip().split('\t')
                if len(f) < 4:
                    continue
                chr   = f[0]
                start = int(f[1])
                end   = int(f[2])
                gene  = f[3]
                if chr not in self.dict:
                    self.dict[chr] = {}
                self.dict[chr][gene] = (start, end)

    def lookup(self, chromosome, location):
        """ Lookup your gene. Returns the gene name """
        cdef int l
        l = int(location)
        answer = ''
        for k, v in self.dict[chromosome].items():
            if v[0] < l < v[1]:
                answer = k
                break
        if answer:
            return answer
        else:
            return None

完整的项目在这里:https://github.com/MikeDacre/python_bed_lookup,虽然整个相关 class 在上面。

代码的问题在于生成的 class/dictionary 占用了人类基因组的大量内存,其中包含 1.1 亿个基因(这是一个 1.1 亿行长的文本文件)。大约两分钟后,当它达到 16GB 内存时,我在构建字典的过程中杀死了 init 函数。任何占用那么多内存的东西基本上都是无用的。

我确信我必须有一种更有效的方法来执行此操作,但我不是 C 程序员,而且我对 cython 还很陌生。我的猜测是我可以构建某种纯 C 结构来保存基因名称以及起始值和结束值。然后我可以将 lookup() 转换为调用另一个名为 _lookup() 的 cdef 函数的函数,并使用该 cdef 函数执行实际查询。

在一个理想的世界中,整个结构可以存在于内存中并且占用不到 2GB 的内存来容纳约 2,000,000 个条目(每个条目有两个整数和一个字符串)。

编辑: 我想出了如何使用 sqlite 高效地处理大文件,要查看完整的 sqlite 代码,请参见此处:https://github.com/MikeDacre/python_bed_lookup

不过,我仍然认为上面的 class 可以用 cython 进行优化,使字典在内存中更小,查找速度更快,欢迎任何建议。

谢谢!

为了扩展我在评论中的建议,不要将 (start,end) 存储为元组,而是将其存储为简单的 Cython 定义的 class:

cdef class StartEnd:
    cdef public int start, end

    def __init__(self, start, end):
        self.start = start
        self.end = end

(您也可以尝试更改整数类型以节省更多空间)。我不建议摆脱 Python 词典,因为它们易于使用,并且(我相信)经过优化以在字符串键的情况下(在 Python 中很常见)相当有效。

我们可以使用 sys.getsizeof 来估算粗略的大小节省。 (请注意,这对内置 classes 和 Cython classes 效果很好,但对 Python classes 效果不佳,所以不要太相信它远。另请注意,结果取决于平台,因此您的结果可能略有不同)。

>>> sys.getsizeof((1,2)) # tuple
64
>>> sys.getsizeof(1) # Python int
28

(因此每个元组包含64+28+28=120字节)

>>> sys.getsizeof(StartEnd(1,2)) # my custom class
24

(24 有意义:它是 PyObject_Head(16 字节:一个 64 位整数和一个指针)+ 2 个 32 位整数)。

因此,小了5倍,我认为这是一个好的开始。

根据我对 cythonnumpy 的有限经验,使用 cython 进行不需要使用 [=33] 的 'inner' 计算是最有利可图的=] 代码。它们是可以转换为紧凑且快速的 C 代码的迭代。

这里重写了您的代码,拆分出两个 classes 可以重铸为 Cython/C 结构:

# cython candidate, like DavidW's StartEnd
class Gene(object):
    def __init__(self, values):
        self.chr = values[0]
        self.start = int(values[1])
        self.end = int(values[2])
        self.gene = values[3]
    def find(self, i):
        return self.start<=i<self.end
    def __repr__(self):
        return "%s(%s, %d:%d)"%(self.chr,self.gene,self.start,self.end)

# cython candidate
class Chrom(list):
    def add(self, aGene):
        self.append(aGene)
    def find(self, loc):
        # find - analogous to string find?
        i = int(loc)
        for gene in self:
             if gene.find(i):
                 return gene  # gene.gene
        return None
    def __repr__(self):
        astr = []
        for gene in self:
            astr += [repr(gene)]
        return '; '.join(astr)

这些将由不需要在 Cython .pdx 文件中的高级 Python 函数(或 class)导入和使用:

from collections import defaultdict
def load(anIterable):
    data = defaultdict(Chrom)
    for line in anIterable:
        f = line.rstrip().split(',')
        if len(f)<4:
            continue
        aGene = Gene(f)
        data[aGene.chr].add(aGene)
    return data

与文件或文本模拟一起使用:

# befile = 'filename'
# with open(bedfile) as infile:
#    data = load(infile)

txt = """\
A, 1,4,a
A, 4,8,b
B, 3,5,a
B, 5,10,c
"""
data = load(txt.splitlines())
print data
# defaultdict(<class '__main__.Chrom'>, {
#   'A': A(a, 1:4); A(b, 4:8),
#   'B': B(a, 3:5); B(c, 5:10)})

print 3, data['A'].find(3)   # a gene
print 9, data['B'].find(9)   # c gene
print 11,data['B'].find(11)   # none

我可以定义一个 find 函数,该函数如果可用则遵从某个方法,否则使用它自己的方法。这类似于委托给方法的 numpy 函数:

def find(chrdata, loc):
    # find - analogous to string find?
    fn = getattr(chrdata, 'find',None)
    if fn is None:
        #raise AttributeError(chrdata,'does not have find method')
        def fn(loc):
            i = int(loc)
            for gene in chrdata:
                if gene.find(i):
                    return gene  # gene.gene
            return None
    return fn(loc)

print 3, find(data['A'],3)

用普通列表数据结构测试find

def loadlist(anIterable):
    # collect data in ordinary list
    data = defaultdict(list)
    for line in anIterable:
        f = line.rstrip().split(',')
        if len(f)<4:
            continue
        aGene = Gene(f)
        data[aGene.chr].append(aGene)
    return data

data = loadlist(txt.splitlines())
print 3, find(data['A'],3)