2012-01-08 17 views
5

Al perfilar la aplicación de mi Python, descubrí que len() parece ser muy costoso cuando se utilizan conjuntos. Ver el código de abajo:Rendimiento perfilado de len (conjunto) frente a conjunto .__ len __() en Python 3

import cProfile 

def lenA(s): 
    for i in range(1000000): 
     len(s); 

def lenB(s): 
    for i in range(1000000): 
     s.__len__(); 

def main(): 
    s = set(); 
    lenA(s); 
    lenB(s); 

if __name__ == "__main__": 
    cProfile.run("main()","stats"); 

De acuerdo con las estadísticas del perfil del abajo, lenA() parece ser 14 veces más lento que lenB():

ncalls tottime percall cumtime percall filename:lineno(function) 
     1 1.986 1.986 3.830 3.830 .../lentest.py:5(lenA) 
1000000 1.845 0.000 1.845 0.000 {built-in method len} 
     1 0.273 0.273 0.273 0.273 .../lentest.py:9(lenB) 

Me estoy perdiendo algo? Actualmente uso __len__() en lugar de len(), pero el código se ve sucio :(

+7

¿Por qué está usando 'cProfile' en lugar de' timeit'? El primero es para encontrar cuellos de botella en programas grandes, y sacrifica cierta precisión a pequeña escala por ello. El último es para medir el rendimiento general de pequeños fragmentos con relativa precisión. 'timeit' debería ser la primera opción para microbenchmarks como este. Y para mí, indica una diferencia menos extrema (0.0879 μs por llamada 'len', 0.158 μs por' .__ len__' call => 'len' siendo 70% más lenta). – delnan

+0

Gracias @delnan, soy bastante nuevo en Python. Usando 'timeit' también obtengo una relación similar. De hecho, mi programa es mucho más grande que el código anterior, pero me sorprendió que la función 'len()' apareciera como uno de los principales cuellos de botella. OK, entonces simplemente ignoraré 'len()' y me concentraré en mis propias funciones, ¿verdad? – Tregoreg

Respuesta

13

Obviamente, len tiene algo de sobrecarga, ya que hace una llamada de función y traduce AttributeError a TypeError. Además, set.__len__ es una operación tan simple que es obligado a ser muy rápido en comparación con casi cualquier cosa, pero todavía no encuentro nada parecido a la diferencia 14x cuando se utiliza timeit:

In [1]: s = set() 

In [2]: %timeit s.__len__() 
1000000 loops, best of 3: 197 ns per loop 

In [3]: %timeit len(s) 
10000000 loops, best of 3: 130 ns per loop 

siempre debe llamar simplemente len, no __len__ Si la llamada a len es el. cuello de botella en su programa, debe reconsiderar su diseño, p. almacenar en caché los tamaños en algún lugar o calcularlos sin llamar al len.

+0

+1: en particular, no optimice prematuramente. Los puntos de referencia pueden ser defectuosos, y como habrá visto ahora, es probable que tres puntos de referencia arrojen tres resultados diferentes; y es posible que termine haciendo una evaluación comparativa de algo completamente diferente de lo que esperaba con una micro-referencia. Obviamente, 'len' no puede ser más rápido, ya que llama' __len__'. Pero eso es todo lo que es seguro. –

+2

@ Anony-Mousse: en realidad, miré mis propios resultados nuevamente y solo ahora veo que 'len' es más rápido que' __len__'. No estoy seguro de cómo sucedió eso. –

+2

's .__ len__' hace una llamada de función también, * y * tiene que buscar un atributo. Eso supera la búsqueda global de 'len'. – WolframH

1

Esto iba a ser un comentario, pero después del comentario de larsman sobre sus polémicos resultados y el resultado que obtuve, creo que es interesante agregar mis datos al hilo.

Tratando más o menos la misma configuración que tengo el contrario el OP consiguió, y en la misma dirección comentado por larsman:

12.1964105975 <- __len__ 
6.22144670823 <- len() 

C:\Python26\programas> 

La prueba:

def lenA(s): 
    for i in range(100): 
     len(s); 

def lenB(s): 
    for i in range(100): 
     s.__len__(); 

s = set() 

if __name__ == "__main__": 

    from timeit import timeit 
    print timeit("lenB(s)", setup="from __main__ import lenB, s") 
    print timeit("lenA(s)", setup="from __main__ import lenA, s") 

Ésta es ActivePython 2.6. 7 64bit en win7

3

Esta es una observación interesante sobre el generador de perfiles, que no tiene nada que ver con el rendimiento real de la función len. Usted ve, en las estadísticas de perfil, hay dos líneas correspondientes a lenA:

ncalls tottime percall cumtime percall filename:lineno(function) 
     1 1.986 1.986 3.830 3.830 .../lentest.py:5(lenA) 
1000000 1.845 0.000 1.845 0.000 {built-in method len} 

... mientras que sólo hay una línea relativa lenB:

 1 0.273 0.273 0.273 0.273 .../lentest.py:9(lenB) 

El perfilador ha superado el tiempo de cada sola llamada de lenA a len, pero el tiempo lenB como un todo. Sincronizar una llamada siempre agrega un poco de sobrecarga; en el caso de lenA, ve este gasto general multiplicado un millón de veces.

+1

Creo que su punto es absolutamente preciso. Se trata de la sobrecarga de 'cProfile', no del funcionamiento de la función' len'. – Tregoreg

Cuestiones relacionadas