为什么使用enumerate循环比使用生成器快得多?

3

最近,在这个线程中,我在Jon Clements的帮助下发现以下代码具有非常不同的执行时间。

你有任何想法为什么会出现这种情况吗?

评论:self.stream_data是一个由许多零和int16值组成的向量元组,create_ZS_data方法正在执行所谓的ZeroSuppression。

环境
输入:许多(3.5k)小文件(每个约120kb)
操作系统:Linux64
Python版本:2.6.8

基于生成器的解决方案:

def create_ZS_data(self):
    self.ZS_data = ( [column, row, self.stream_data[column + row * self.rows ]]
                     for row, column in itertools.product(xrange(self.rows), xrange(self.columns))
                     if self.stream_data[column + row * self.rows ] )

性能分析信息:

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
     3257    1.117    0.000   71.598    0.022 decode_from_merlin.py:302(create_ZS_file)
   463419   67.705    0.000   67.705    0.000 decode_from_merlin.py:86(<genexpr>)

Jon的解决方案:

create_ZS_data(self):
    self.ZS_data = list()
    for rowno, cols in enumerate(self.stream_data[i:i+self.columns] for i in xrange(0, len(self.stream_data), self.columns)):
        for colno, col in enumerate(cols):
            # col == value, (rowno, colno) = index
            if col:
                self.ZS_data.append([colno, rowno, col])


分析器信息:

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
     3257   18.616    0.006   19.919    0.006 decode_from_merlin.py:83(create_ZS_data)

1
这似乎非常明显与第一个解决方案中的调用数量有关,不是吗? - martineau
是的,但生成器不是为了处理大量调用而诞生的吗?它们通常被推荐作为大型列表的替代方案。 - Michal
1
对于简单的情况而言,生成器通常以 CPU 为代价来降低内存使用。 - Hamish
你的第一个解决方案实际上并没有使用生成器表达式,而是列表推导式。 - user3850
@martineau:那又怎样?这并不与我所说的相矛盾... - user3850
显示剩余2条评论
2个回答

4

我看了之前的讨论,你似乎很困扰你的聪明理解在循环中不如源代码字符那样高效。但我当时没有指出的是,这将是我更喜欢的实现方式:

def sparse_table_elements(cells, columns, rows):
    ncells = len(cells)
    non_zeros = list()
    for nrow in range(0, ncells, columns):
         row = cells[nrow:nrow+columns]
         for ncol, cell in enumerate(row):
             if cell:
                 non_zeros.append([ncol, nrow, cell])
    return non_zeros

我没有测试过它,但我可以理解它。有几件事情让我感到有些潜在的低效率。重新计算两个恒定单调“无聊”索引的笛卡尔积肯定是很昂贵的:

itertools.product(xrange(self.rows), xrange(self.columns))

然后您可以使用结果[(0, 0), (0, 1), ...]从源中进行单个元素索引:

stream_data[column + row * self.rows]

"Jon's"实现方式可以处理更大的片段,因此比这种方法更加节省成本。

生成器并不能保证高效。在这种情况下,已经读入核心的135kb数据可能会因为构造不良的生成器而导致效率降低。如果您需要简洁的矩阵运算,可以使用APL;如果您想要可读性强的代码,就不要在Python中追求过分的最小化。


此外,您所提到的“单元素切片”——我简单地称之为对stream_data进行索引访问——在生成器/列表推导式版本中实际上被执行了两次。 - martineau

2
你可以轻松地将Jon的解决方案重写为生成器:
def create_ZS_data(self):
    self.ZS_data = ([colno, rowno, col]
                    for rowno, cols in enumerate(self.stream_data[i:i+self.columns]
                                                 for i in xrange(0, len(self.stream_data), self.columns))
                    for colno, col in enumerate(cols)
                    if col)

我强烈期望这个代码的表现和Jon基于循环的解决方案一样,这证明了性能差异在于算法实现的中等规模细节。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接