rapidité yield vs for

hello, les pythonistes!
il y en a t’il un qui saurait me dire pourquoi une boucle for est environ 30% plus rapide q’un yield from?

je n’arrive pas à comprendre pourquoi

def _for():
    start = time.time()
    for i in range(8000):
        for j in range(i):
            yield j
    print(time.time() - start)


def _yield():
    start = time.time()
    yield from (j for i in range(8000) for j in range(i))
    print(time.time() - start)

>>> list(_for())
1.9730029106140137

>>>list(_yield())
2.552983045578003
1 « J'aime »

Bonjour Paulo,

Pas sur mais avec from dis import dis on peut voir les scope embriqués et un make et un appel de fonction supplémentaire - à priori les appels de fonction prennent du temps sous Python

-Graham

Yield from

 dis('''def _yield(): 
   ...:     yield from (j for i in range(8000) for j in range(i)) 
   ...:      
   ...: list(_yield())''')                                                      
  1           0 LOAD_CONST               0 (<code object _yield at 0x10377ef60, file "<dis>", line 1>)
              2 LOAD_CONST               1 ('_yield')
              4 MAKE_FUNCTION            0
              6 STORE_NAME               0 (_yield)

  4           8 LOAD_NAME                1 (list)
             10 LOAD_NAME                0 (_yield)
             12 CALL_FUNCTION            0
             14 CALL_FUNCTION            1
             16 POP_TOP
             18 LOAD_CONST               2 (None)
             20 RETURN_VALUE

Disassembly of <code object _yield at 0x10377ef60, file "<dis>", line 1>:
  2           0 LOAD_CONST               1 (<code object <genexpr> at 0x10377ec90, file "<dis>", line 2>)
              2 LOAD_CONST               2 ('_yield.<locals>.<genexpr>')
              4 MAKE_FUNCTION            0
              6 LOAD_GLOBAL              0 (range)
              8 LOAD_CONST               3 (8000)
             10 CALL_FUNCTION            1
             12 GET_ITER
             14 CALL_FUNCTION            1
             16 GET_YIELD_FROM_ITER
             18 LOAD_CONST               0 (None)
             20 YIELD_FROM
             22 POP_TOP
             24 LOAD_CONST               0 (None)
             26 RETURN_VALUE

Disassembly of <code object <genexpr> at 0x10377ec90, file "<dis>", line 2>:
  2           0 LOAD_FAST                0 (.0)
        >>    2 FOR_ITER                24 (to 28)
              4 STORE_FAST               1 (i)
              6 LOAD_GLOBAL              0 (range)
              8 LOAD_FAST                1 (i)
             10 CALL_FUNCTION            1
             12 GET_ITER
        >>   14 FOR_ITER                10 (to 26)
             16 STORE_FAST               2 (j)
             18 LOAD_FAST                2 (j)
             20 YIELD_VALUE
             22 POP_TOP
             24 JUMP_ABSOLUTE           14
        >>   26 JUMP_ABSOLUTE            2
        >>   28 LOAD_CONST               0 (None)
             30 RETURN_VALUE

For … yield

dis('''def _for(): 
   ...:     for i in range(8000): 
   ...:         for j in range(i): 
   ...:             yield j 
   ...: list(_for())''')                                                        
  1           0 LOAD_CONST               0 (<code object _for at 0x1035c2b70, file "<dis>", line 1>)
              2 LOAD_CONST               1 ('_for')
              4 MAKE_FUNCTION            0
              6 STORE_NAME               0 (_for)

  5           8 LOAD_NAME                1 (list)
             10 LOAD_NAME                0 (_for)
             12 CALL_FUNCTION            0
             14 CALL_FUNCTION            1
             16 POP_TOP
             18 LOAD_CONST               2 (None)
             20 RETURN_VALUE

Disassembly of <code object _for at 0x1035c2b70, file "<dis>", line 1>:
  2           0 SETUP_LOOP              40 (to 42)
              2 LOAD_GLOBAL              0 (range)
              4 LOAD_CONST               1 (8000)
              6 CALL_FUNCTION            1
              8 GET_ITER
        >>   10 FOR_ITER                28 (to 40)
             12 STORE_FAST               0 (i)

  3          14 SETUP_LOOP              22 (to 38)
             16 LOAD_GLOBAL              0 (range)
             18 LOAD_FAST                0 (i)
             20 CALL_FUNCTION            1
             22 GET_ITER
        >>   24 FOR_ITER                10 (to 36)
             26 STORE_FAST               1 (j)

  4          28 LOAD_FAST                1 (j)
             30 YIELD_VALUE
             32 POP_TOP
             34 JUMP_ABSOLUTE           24
        >>   36 POP_BLOCK
        >>   38 JUMP_ABSOLUTE           10
        >>   40 POP_BLOCK
        >>   42 LOAD_CONST               0 (None)
             44 RETURN_VALUE
1 « J'aime »

Si j’ai tout compris ce serait l’appel de from qui prendrait du temps ?
30% cela fait beaucoup quand même

Autre option aussi rapide que for selon mes tests:

def _gen(): 
     return(j for i in range(8000) for j in range(i))

Assez d’accord avec ce qui a été dit :

_for est implémenté en une seule fonction, elle n’appelle que range

_yield est implémentée en deux fonctions : yield et l’expression génératrice (qui, sous le capot, est une fonction), et on fait l’aller retour d’une fonction à l’autre un bon nombre de fois (8000+7999+7998+… fois), ce qui a un coût.

Donc oui, c’est l’appel de fonction qui est coûteux (c’est toute une machinerie, l’appel d’une fonction), mais conclure “houla un appel de fonction coûte 30% du temps de calcul” n’est vrai que dans ce cas précis : c’est vrais ici parce que la fonction ne fais quasiment rien, donc le temps de l’appel commence à sacrément apparaître, mais pour une fonction qui fait “quelque chose”, le temps de l’appel serait négligable.

Pour la solution de @dancergraham, j’aurais tendance à mettre une espace après return, sinon ça ressemble à un appel de fonction, ce qui me gêne (dans le sens où ça peut gêner un débutant qui aurait encore du mal a faire la distinction “appel de fonction / sortie d’une fonction” (aller dans un sens ou dans l’autre dans la pile d’appels).

1 « J'aime »