Las llamadas a la función Python son muy lentas

Esto se debe principalmente a garantizar que mi metodología sea correcta, pero mi pregunta básica fue si vale la pena comprobar fuera de una función si necesito acceder a ella. Lo sé, lo sé, la optimización prematura, pero en muchos casos, es la diferencia entre poner una instrucción if dentro de la llamada de función para determinar si necesito ejecutar el rest del código, o ponerlo antes de la llamada de función. En otras palabras, no hace falta esfuerzo para hacerlo de una manera u otra. En este momento, todos los controles se mezclan entre ambos, y me gustaría que todo esté bien y estandarizado.

La razón principal por la que pregunté es porque las otras respuestas que vi en su mayoría se referían a timeit, pero eso me dio números negativos, así que cambié a esto:

import timeit import cProfile def aaaa(idd): return idd def main(): #start = timeit.timeit() for i in range(9999999): a = 5 #end = timeit.timeit() #print("1", end - start) def main2(): #start = timeit.timeit() for i in range(9999999): aaaa(5) #end = timeit.timeit() #print("2", end - start) cProfile.run('main()', sort='cumulative') cProfile.run('main2()', sort='cumulative') 

y obtuve esto para salida

  ncalls tottime percall cumtime percall filename:lineno(function) 1 0.000 0.000 0.310 0.310 {built-in method exec} 1 0.000 0.000 0.310 0.310 :1() 1 0.310 0.310 0.310 0.310 test.py:7(main) 1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects} ncalls tottime percall cumtime percall filename:lineno(function) 1 0.000 0.000 2.044 2.044 {built-in method exec} 1 0.000 0.000 2.044 2.044 :1() 1 1.522 1.522 2.044 2.044 test.py:14(main2) 9999999 0.521 0.000 0.521 0.000 test.py:4(aaaa) 

Para mí, eso demuestra que no llamar a la función es de .31 segundos, y llamarlo toma 1.52 segundos, lo cual es casi 5 veces más lento. Pero como dije, obtuve números negativos con el tiempo, así que quiero asegurarme de que en realidad sea tan lento.

Además, por lo que reconozco, la razón por la que las llamadas a la función son tan lentas es porque Python necesita buscar para asegurarse de que la función aún existe antes de poder ejecutarla o algo así. ¿No hay alguna manera de decirle que le guste … asumir que todo sigue ahí para que no tenga que hacer un trabajo innecesario que (aparentemente) lo ralentiza 5 veces?

    Estás comparando manzanas y peras aquí. Un método hace una asignación simple, el otro llama a una función. Sí, las llamadas de función agregarán gastos generales.

    Deberías reducir esto al mínimo para timeit :

     >>> import timeit >>> timeit.timeit('a = 5') 0.03456282615661621 >>> timeit.timeit('foo()', 'def foo(): a = 5') 0.14389896392822266 

    Ahora todo lo que hicimos fue agregar una llamada de función ( foo hace lo mismo), para que pueda medir el tiempo extra que toma una llamada de función. No puede decir que esto es casi 4 veces más lento, no, la llamada a la función agrega una sobrecarga de 0.11 segundos para 1,000,000 iteraciones.

    Si en lugar de a = 5 hacemos algo que toma 0.5 segundos para ejecutar un millón de iteraciones, moverlas a una función no hará que las cosas tomen 2 segundos. Ahora tomará 0.61 segundos porque la sobrecarga de la función no crece.

    Una llamada a la función necesita manipular la stack, empujando el marco local en ella, creando un nuevo marco, y luego borrarlo todo de nuevo cuando la función regrese.

    En otras palabras, mover las declaraciones a una función agrega una pequeña sobrecarga, y cuantas más afirmaciones se muevan a esa función, menor será la sobrecarga como porcentaje del trabajo total realizado. Una función nunca hace esas afirmaciones más lentas.

    Una función de Python es solo un objeto almacenado en una variable; puede asignar funciones a una variable diferente, reemplazarlas por algo completamente diferente o eliminarlas en cualquier momento. Cuando invoca una función, primero hace referencia al nombre por el que se almacenan ( foo ) y luego invoca el objeto de función ( (arguments) ); esa búsqueda tiene que ocurrir cada vez en un lenguaje dynamic.

    Puedes ver esto en el bytecode generado para una función:

     >>> def foo(): ... pass ... >>> def bar(): ... return foo() ... >>> import dis >>> dis.dis(bar) 2 0 LOAD_GLOBAL 0 (foo) 3 CALL_FUNCTION 0 6 RETURN_VALUE 

    El LOAD_GLOBAL operación LOAD_GLOBAL busca el nombre ( foo ) en el espacio de nombres global (básicamente una búsqueda de tabla hash), y coloca el resultado en la stack. CALL_FUNCTION luego invoca lo que está en la stack, reemplazándolo con el valor de retorno. RETURN_VALUE regresa de una llamada de función, nuevamente toma lo que está más arriba en la stack como valor de retorno.