1
littleMaple 2021-07-25 05:06:22 +08:00
这种原生语法的速度比较需要讨论到底层实现,生成器要维护一个底层的协程,推导式不用,这是我能想到的其中一个对速度有影响的区别。
|
2
O5oz6z3 OP @littleMaple 确实还有这个因素,也就是说运行开销生成器更重一些,推导式更轻一些。至于底层实现我就不太清楚了……
|
3
Richard14 2021-07-25 06:52:56 +08:00
评价:理所当然
|
4
O5oz6z3 OP @Richard14 好吧,看来是我问得不够具体。虽然我也觉得显而易见,但是纸上谈兵不够肯定。比如说考虑到运行开销,连串嵌套的生成器应该慢一些;考虑到内存分配速度,也许是推导式慢一些。
虽然可能归根到底,这两者的速度差距其实无关紧要。 |
5
renmu123 2021-07-25 10:05:12 +08:00 via Android
这两者主要是内存上的差异吧
|
6
billlee 2021-07-25 15:55:40 +08:00
python 就不要考虑性能了吧
|
7
WilliamYang 2021-07-25 18:14:35 +08:00
@billlee 完全同意,现在还考虑 Python 怎么写性能好些,真的觉得像是很无聊的事
|
8
O5oz6z3 OP @renmu123 同意,只是突然想到可以将速度也作为生成器和推导式的其中一种区别,所以抛出这么一个问题。
@billlee @WilliamYang 确实,不知道有多少人经常拿动态语言和静态语言比…… |
9
abersheeran 2021-07-25 20:39:41 +08:00
In [1]: import dis
In [2]: dis.dis("[x for x in iter]") 1 0 LOAD_CONST 0 (<code object <listcomp> at 0x000001F48BAC25D0, file "<dis>", line 1>) 2 LOAD_CONST 1 ('<listcomp>') 4 MAKE_FUNCTION 0 6 LOAD_NAME 0 (iter) 8 GET_ITER 10 CALL_FUNCTION 1 12 RETURN_VALUE Disassembly of <code object <listcomp> at 0x000001F48BAC25D0, file "<dis>", line 1>: 1 0 BUILD_LIST 0 2 LOAD_FAST 0 (.0) >> 4 FOR_ITER 8 (to 14) 6 STORE_FAST 1 (x) 8 LOAD_FAST 1 (x) 10 LIST_APPEND 2 12 JUMP_ABSOLUTE 4 >> 14 RETURN_VALUE In [3]: dis.dis("(x for x in iter)") 1 0 LOAD_CONST 0 (<code object <genexpr> at 0x000001F48C359030, file "<dis>", line 1>) 2 LOAD_CONST 1 ('<genexpr>') 4 MAKE_FUNCTION 0 6 LOAD_NAME 0 (iter) 8 GET_ITER 10 CALL_FUNCTION 1 12 RETURN_VALUE Disassembly of <code object <genexpr> at 0x000001F48C359030, file "<dis>", line 1>: 1 0 LOAD_FAST 0 (.0) >> 2 FOR_ITER 10 (to 14) 4 STORE_FAST 1 (x) 6 LOAD_FAST 1 (x) 8 YIELD_VALUE 10 POP_TOP 12 JUMP_ABSOLUTE 2 >> 14 LOAD_CONST 0 (None) 16 RETURN_VALUE |
10
O5oz6z3 OP |
11
O5oz6z3 OP @O5oz6z3 #10
临时也想不出什么比较好的用来测试的逻辑: import timeit _iter = list(range(12345)) init = 'iter = _iter' end = 'iter = list(iter)' maketest = lambda stmt, n: '\n'.join([init, *[stmt]*n, end]) |
12
O5oz6z3 OP (测试)
comp = 'iter = [x**0.5 for x in iter]' timeit.timeit(maketest(comp, 3), number=100, globals=globals()) |
13
O5oz6z3 OP @O5oz6z3 #10
import timeit _iter = list(range(12345)) init = 'iter = _iter' end = 'iter = list(iter)' maketest = lambda stmt, n: '\n'.join([init, *[stmt]*n, end]) comp = 'iter = [x**0.5 for x in iter]' >>> timeit.timeit(maketest(comp, 3), number=100, globals=globals()) 1.33 >>> timeit.timeit(maketest(comp, 10), number=100, globals=globals()) 4.26 gene = 'iter = (x**0.5 for x in iter)' >>> timeit.timeit(maketest(gene, 3), number=100, globals=globals()) 1.63 >>> timeit.timeit(maketest(gene, 10), number=100, globals=globals()) 4.60 |
14
fakepoet 2021-07-26 12:33:08 +08:00 1
我觉得这是一个非常有趣的问题,花了点时间搜索了一下,stackoverflow 上的[这个回答]( https://stackoverflow.com/questions/16307326/why-this-list-comprehension-is-faster-than-equivalent-generator-expression)做出的解释看上去可能性比较大,简而言之就是从 Bytecode 中看两者的差异主要是`POP_TOP`这条指令,所以怀疑生成器底层有队列的实现维护生成好的值,而 pop 操作是导致花费更多运行时间的原因。
|
15
fakepoet 2021-07-26 12:33:44 +08:00
(我是真的不会用 v2 的 markdown...
|
16
zachlhb 2021-07-26 18:14:14 +08:00 via iPhone
说的示例不都是推导式吗,一个是列表推导式,一个是元组推导式
|
17
jaredyam 2021-07-26 23:10:29 +08:00
|
18
O5oz6z3 OP @fakepoet #14 我也觉得很有趣,虽然实际意义不大……
底层队列什么的我就不懂了,不过我的看法也和链接里的一样:通常情况下内存不是瓶颈,所以速度上推导式略胜一筹,但其实两者的差距非常小可以忽略不计。也就是推导式速度不是快很多,但生成器非常省内存。 @zachlhb #16 如果我没搞错的话,所谓的元组推导式正式名称就是生成器表达式,虽然生成器不止这一种写法。 @fakepoet @jaredyam #15 (可能这是 V2EX 特色,让用户在每次回复时摸索出真正的语法,增加趣味性) 顺带一提,#13 楼的写法又抛出了另一个问题:生成器表达式中的 iter 到底指向的是当时的值还是说引用全局标识符 iter ?如果是后者那么就乱套了…… |
19
imn1 2021-07-31 15:16:55 +08:00
生成器主要是语句执行时并不运算,而是后面调用的时候才一并运算,推导式是语句执行同时计算相关值
例如有一个列表,变形得出相应值,但是后续是根据 if 分支是否使用这些值,这时候用生成器比较好,需要时才计算;在不需要时这个生成器并不会耗费太多时间和算力;又或者类似地两个列表,后续根据 if 二选一,两个都直接计算的话,其中一个必然浪费时间和算力,用生成器就省一些 #18 local 变量优先,如果本地变量名和一些全局名称相同的话,会使用本地变量名,当然有部份关键字是不能用于命名的 例如 list = (1,2,3) list(range(5)) # 这句就会报错 @zachlhb #16 元组推导式是 tuple(x for x in iter) |
20
O5oz6z3 OP @imn1 感谢回复……
关于 #18 楼提到的 #13 楼的问题,是我说得太含糊了,让人理解成本地变量名覆盖全局内置函数名称的软关键字问题。机会难得我就顺便补充一下。 举个例子: iter = [1, 2, 3, 4] iter = (x+2 for x in iter) iter = (x+3 for x in iter) 以上代码中,第三行右侧的 `(x+3 for x in iter)` 表达式中的 iter 迭代时会展开为第二行右侧的生成器表达式 `(x+2 for x in iter)`。问题就是此时第二行右侧生成器中的 iter 指的是哪个 iter ?是 `(x+2 for x in iter)` 还是第一行的 [1, 2, 3, 4]? 后来实际验证了一下,答案是第一行的 [1, 2, 3, 4],也就是生成器表达式中的 iter 指向定义时候的 iter 变量的值,而不是执行的时候动态引用作用域中 iter 变量的值。有点像是闭包保存了定义时候的作用域。 |