为什么这个生成器表达式函数比循环版本慢?
Why is this generator expression function slower than the loop version?
我一直根据生成器表达式往往比普通循环更有效的理论进行操作。但是后来我 运行 进入以下示例:编写一个函数,给定一个数字 N
和一些因素 ps
,returns 下所有数字的总和 N
是至少一个因数的倍数。
这是一个循环版本和一个更短的生成器表达式版本:
def loops(N, ps):
total_sum = 0
for i in xrange(N):
for p in ps:
if i%p == 0:
total_sum += i
break
return total_sum
def genexp(N, ps):
return sum(i for i in xrange(N)
if any(i%p == 0 for p in ps))
本以为两者的表现大致相当,理解版可能快一点,没想到的是:
for func in ('loops', 'genexp'):
print func, timeit.timeit('%s(100000, [3,5,7])' % func,
number=100,
setup='from __main__ import %s' % func)
loops 2.82878184319
genexp 10.1663100719
慢 4 倍还差得远!为什么?我误会了什么?
首先:生成器表达式内存高效,不一定速度高效。
您的紧凑 genexp()
版本较慢,原因有二:
生成器表达式是使用新范围(如新函数)实现的。您正在生成 N 个新范围,每个 any()
测试一个。创建一个新范围并再次拆除它的成本相对较高,当然是在循环中完成,然后与不这样做的代码进行比较。
sum()
和 any()
名称是要查找的附加全局变量。在 any()
的情况下,这是每个测试额外的 N 全局查找。必须在字典中查找全局变量,而局部变量则通过 C 数组中的索引查找(速度非常快)。
后者只是一个小组件,大部分开销都在创建和销毁框架(作用域);如果您创建一个版本,其中 _any
和 _sum
是您获得的函数的局部变量,但性能略有提高:
>>> def genexp_locals(N, ps, _any=any, _sum=sum):
... return _sum(i for i in xrange(N)
... if _any(i%p == 0 for p in ps))
...
>>> for func in ('loops', 'genexp', 'genexp_locals'):
... print func, timeit.timeit('%s(100000, [3,5,7])' % func,
... number=100,
... setup='from __main__ import %s' % func)
...
loops 2.00835800171
genexp 6.45241594315
genexp_locals 6.23843789101
我没有为 xrange
创建本地文件以保持该方面相同。从技术上讲,生成器表达式代码对象将 _any
名称作为闭包而不是本地查找,这不像全局查找那么慢,但也没有本地查找那么快。
我一直根据生成器表达式往往比普通循环更有效的理论进行操作。但是后来我 运行 进入以下示例:编写一个函数,给定一个数字 N
和一些因素 ps
,returns 下所有数字的总和 N
是至少一个因数的倍数。
这是一个循环版本和一个更短的生成器表达式版本:
def loops(N, ps):
total_sum = 0
for i in xrange(N):
for p in ps:
if i%p == 0:
total_sum += i
break
return total_sum
def genexp(N, ps):
return sum(i for i in xrange(N)
if any(i%p == 0 for p in ps))
本以为两者的表现大致相当,理解版可能快一点,没想到的是:
for func in ('loops', 'genexp'):
print func, timeit.timeit('%s(100000, [3,5,7])' % func,
number=100,
setup='from __main__ import %s' % func)
loops 2.82878184319
genexp 10.1663100719
慢 4 倍还差得远!为什么?我误会了什么?
首先:生成器表达式内存高效,不一定速度高效。
您的紧凑 genexp()
版本较慢,原因有二:
生成器表达式是使用新范围(如新函数)实现的。您正在生成 N 个新范围,每个
any()
测试一个。创建一个新范围并再次拆除它的成本相对较高,当然是在循环中完成,然后与不这样做的代码进行比较。sum()
和any()
名称是要查找的附加全局变量。在any()
的情况下,这是每个测试额外的 N 全局查找。必须在字典中查找全局变量,而局部变量则通过 C 数组中的索引查找(速度非常快)。
后者只是一个小组件,大部分开销都在创建和销毁框架(作用域);如果您创建一个版本,其中 _any
和 _sum
是您获得的函数的局部变量,但性能略有提高:
>>> def genexp_locals(N, ps, _any=any, _sum=sum):
... return _sum(i for i in xrange(N)
... if _any(i%p == 0 for p in ps))
...
>>> for func in ('loops', 'genexp', 'genexp_locals'):
... print func, timeit.timeit('%s(100000, [3,5,7])' % func,
... number=100,
... setup='from __main__ import %s' % func)
...
loops 2.00835800171
genexp 6.45241594315
genexp_locals 6.23843789101
我没有为 xrange
创建本地文件以保持该方面相同。从技术上讲,生成器表达式代码对象将 _any
名称作为闭包而不是本地查找,这不像全局查找那么慢,但也没有本地查找那么快。