生成器函数性能

6 投票
2 回答
772 浏览
提问于 2025-04-16 19:11

我正在尝试理解生成器函数的性能。我使用了 cProfile 和 pstats 模块来收集和检查性能数据。这里讨论的函数是:

def __iter__(self):
    delimiter  = None
    inData     = self.inData
    lenData    = len(inData)
    cursor     = 0
    while cursor < lenData:
        if delimiter:
            mo = self.stringEnd[delimiter].search(inData[cursor:])
        else:
            mo = self.patt.match(inData[cursor:])
        if mo:
            mo_lastgroup = mo.lastgroup
            mstart       = cursor
            mend         = mo.end()
            cursor       += mend
            delimiter = (yield (mo_lastgroup, mo.group(mo_lastgroup), mstart, mend))
        else:
            raise SyntaxError("Unable to tokenize text starting with: \"%s\"" % inData[cursor:cursor+200])

self.inData 是一个 Unicode 文本字符串,self.stringEnd 是一个包含 4 个简单正则表达式的字典,self.patt 是一个大的正则表达式。整个过程是将这个大字符串一个一个地拆分成小字符串。

在分析使用这个函数的程序时,我发现程序运行时间的大部分都花在了这个函数上:

In [800]: st.print_stats("Scanner.py:124")

         463263 function calls (448688 primitive calls) in 13.091 CPU seconds

   Ordered by: cumulative time
   List reduced from 231 to 1 due to restriction <'Scanner.py:124'>

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
    10835   11.465    0.001   11.534    0.001 Scanner.py:124(__iter__)

但是查看这个函数本身的性能分析时,发现它的子函数调用并没有花费太多时间:

In [799]: st.print_callees("Scanner.py:124")
   Ordered by: cumulative time
   List reduced from 231 to 1 due to restriction <'Scanner.py:124'>

Function                  called...
                              ncalls  tottime  cumtime
Scanner.py:124(__iter__)  ->   10834    0.006    0.006  {built-in method end}
                               10834    0.009    0.009  {built-in method group}
                                8028    0.030    0.030  {built-in method match}
                                2806    0.025    0.025  {built-in method search}
                                   1    0.000    0.000  {len}

这个函数的其他部分除了循环、赋值和条件判断外,没什么特别的。即使我使用的生成器的 send 方法也很快:

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
13643/10835    0.007    0.000   11.552    0.001 {method 'send' of 'generator' objects}

难道说 yield,也就是把一个值返回给调用者,花费了大部分时间吗?!还有其他我不知道的原因吗?

编辑

我可能应该提到,生成器函数 __iter__ 是一个小类的方法,所以 self 指的是这个类的一个实例。

2 个回答

1

如果我理解你的例子没错的话,你是把一个生成器对象放进了delimiter里,然后用它来查找数组。这可能不是你速度慢的原因,但我敢肯定这是一处错误。

2

这其实是Dunes的回答,不过他只是在评论里说的,似乎不太想把它写成正式的回答。

主要的问题出在字符串切片上。有些时间测试显示,当切片很大的时候,性能会明显下降(也就是说,从一个已经很大的字符串中再切出一大块)。为了绕过这个问题,我现在在正则表达式对象的方法中使用了pos参数:

    if delimiter:
        mo = self.stringEnd[delimiter].search(inData, pos=cursor)
    else:
        mo = self.patt.match(inData, pos=cursor)

感谢所有提供帮助的人。

撰写回答