¿Por qué [] es más rápido que list()?

Resuelto Augusta asked hace 9 años • 5 respuestas

Comparé las velocidades de procesamiento de []y list()en Python 3.11

$ python -m timeit '[]'
20000000 loops, best of 5: 11.3 nsec per loop
$ python -m timeit 'list()'
10000000 loops, best of 5: 26.1 nsec per loop

y se sorprendió al descubrir que []corre aproximadamente dos veces más rápido que list(). Obtuve resultados muy similares para {}ydict()

$ python -m timeit '{}'
20000000 loops, best of 5: 11.6 nsec per loop
$ python -m timeit 'dict()'
10000000 loops, best of 5: 27.1 nsec per loop

¿Por qué es esto? ¿ []Y {}(y probablemente ()y ''también) devuelven inmediatamente copias de algún literal de stock vacío mientras sus contrapartes nombradas explícitamente ( ,,, list()) se dedican a crear un objeto, tengan o no elementos?dict()tuple()str()

Augusta avatar May 13 '15 20:05 Augusta
Aceptado

Porque []y {}son sintaxis literal . Python puede crear código de bytes solo para crear la lista o los objetos del diccionario:

>>> import dis
>>> dis.dis(compile('[]', '', 'eval'))
  1           0 BUILD_LIST               0
              3 RETURN_VALUE        
>>> dis.dis(compile('{}', '', 'eval'))
  1           0 BUILD_MAP                0
              3 RETURN_VALUE        

list()y dict()son objetos separados. Sus nombres deben resolverse, la pila debe participar para enviar los argumentos, el marco debe almacenarse para recuperarse más tarde y se debe realizar una llamada. Todo eso lleva más tiempo.

Para el caso vacío, eso significa que tiene al menos a LOAD_NAME(que debe buscar en el espacio de nombres global así como en el builtinsmódulo ) seguido de a CALL_FUNCTION, que debe preservar el marco actual:

>>> dis.dis(compile('list()', '', 'eval'))
  1           0 LOAD_NAME                0 (list)
              3 CALL_FUNCTION            0
              6 RETURN_VALUE        
>>> dis.dis(compile('dict()', '', 'eval'))
  1           0 LOAD_NAME                0 (dict)
              3 CALL_FUNCTION            0
              6 RETURN_VALUE        

Puede cronometrar la búsqueda de nombres por separado con timeit:

>>> import timeit
>>> timeit.timeit('list', number=10**7)
0.30749011039733887
>>> timeit.timeit('dict', number=10**7)
0.4215109348297119

La discrepancia horaria probablemente se debe a una colisión de hash del diccionario. Resta esos tiempos de los tiempos para llamar a esos objetos y compara el resultado con los tiempos para usar literales:

>>> timeit.timeit('[]', number=10**7)
0.30478692054748535
>>> timeit.timeit('{}', number=10**7)
0.31482696533203125
>>> timeit.timeit('list()', number=10**7)
0.9991960525512695
>>> timeit.timeit('dict()', number=10**7)
1.0200958251953125

Por lo tanto, tener que llamar al objeto lleva segundos adicionales 1.00 - 0.31 - 0.30 == 0.39por cada 10 millones de llamadas.

Puede evitar el costo de búsqueda global al asignar un alias a los nombres globales como locales (usando una timeitconfiguración, todo lo que vincula a un nombre es local):

>>> timeit.timeit('_list', '_list = list', number=10**7)
0.1866450309753418
>>> timeit.timeit('_dict', '_dict = dict', number=10**7)
0.19016098976135254
>>> timeit.timeit('_list()', '_list = list', number=10**7)
0.841480016708374
>>> timeit.timeit('_dict()', '_dict = dict', number=10**7)
0.7233691215515137

pero nunca podrás superar ese CALL_FUNCTIONcosto.

Martijn Pieters avatar May 13 '2015 13:05 Martijn Pieters

list()requiere una búsqueda global y una llamada a función, pero []se compila en una sola instrucción. Ver:

Python 2.7.3
>>> import dis
>>> dis.dis(lambda: list())
  1           0 LOAD_GLOBAL              0 (list)
              3 CALL_FUNCTION            0
              6 RETURN_VALUE        
>>> dis.dis(lambda: [])
  1           0 BUILD_LIST               0
              3 RETURN_VALUE        
Dan D. avatar May 13 '2015 13:05 Dan D.

Porque listes una función para convertir, por ejemplo, una cadena en un objeto de lista, mientras que []se usa para crear una lista desde el principio. Pruebe esto (puede que tenga más sentido para usted):

x = "wham bam"
a = list(x)
>>> a
["w", "h", "a", "m", ...]

Mientras

y = ["wham bam"]
>>> y
["wham bam"]

Le brinda una lista real que contiene todo lo que puso en ella.

Torxed avatar May 13 '2015 13:05 Torxed

Las respuestas aquí son excelentes, van al grano y cubren completamente esta pregunta. Bajaré un paso más desde el código de bytes para aquellos interesados. Estoy usando el repositorio más reciente de CPython; Las versiones anteriores se comportan de manera similar a este respecto, pero es posible que se realicen ligeros cambios.

Aquí hay un desglose de la ejecución de cada uno de estos, BUILD_LISTpara []y CALL_FUNCTIONpara list().


La BUILD_LISTinstrucción:

Deberías ver el horror:

PyObject *list =  PyList_New(oparg);
if (list == NULL)
    goto error;
while (--oparg >= 0) {
    PyObject *item = POP();
    PyList_SET_ITEM(list, oparg, item);
}
PUSH(list);
DISPATCH();

Terriblemente complicado, lo sé. Así de sencillo es:

  • Cree una nueva lista con PyList_New(esto principalmente asigna la memoria para un nuevo objeto de lista), opargindicando el número de argumentos en la pila. Directo al grano.
  • Comprueba que no salió nada mal if (list==NULL).
  • Agregue cualquier argumento (en nuestro caso, esto no se ejecuta) ubicado en la pila con PyList_SET_ITEM(una macro).

¡No es de extrañar que sea rápido! Está hecho a medida para crear listas nuevas, nada más :-)

La CALL_FUNCTIONinstrucción:

Esto es lo primero que ves cuando echas un vistazo al manejo del código CALL_FUNCTION:

PyObject **sp, *res;
sp = stack_pointer;
res = call_function(&sp, oparg, NULL);
stack_pointer = sp;
PUSH(res);
if (res == NULL) {
    goto error;
}
DISPATCH();

Parece bastante inofensivo, ¿verdad? Bueno, no, desafortunadamente no, call_functionno es un tipo sencillo que llame a la función inmediatamente, no puede hacerlo. En cambio, toma el objeto de la pila, toma todos los argumentos de la pila y luego cambia según el tipo de objeto; Es una:

  • PyCFunction_Type? No, lo es list, listno es de tipo.PyCFunction
  • PyMethodType? No, ver anterior.
  • PyFunctionType? No, ver anterior.

Estamos llamando al listtipo, el argumento pasado call_functiones PyList_Type. CPython ahora tiene que llamar a una función genérica para manejar cualquier objeto invocable llamado _PyObject_FastCallKeywords, sí, más llamadas a funciones.

Esta función vuelve a realizar algunas comprobaciones para ciertos tipos de funciones (que no puedo entender por qué) y luego, después de crear un dict para kwargs si es necesario , continúa llamando _PyObject_FastCallDict.

_PyObject_FastCallDict¡Por fin nos lleva a alguna parte! Después de realizar aún más comprobaciones, agarra la tp_callranura de latype que typehemos pasado, es decir, agarra type.tp_call. Luego procede a crear una tupla a partir de los argumentos pasados _PyStack_AsTuple​​y, finalmente, ¡ finalmente se puede realizar una llamada !

tp_call, que coincide type.__call__toma el control y finalmente crea el objeto de lista. Llama a las listas __new__que corresponden PyType_GenericNewy le asigna memoria PyType_GenericAlloc: Esta es en realidad la parte donde finalmente se pone al díaPyList_New . Todo lo anterior es necesario para manejar objetos de forma genérica.

Al final, type_callllama list.__init__e inicializa la lista con los argumentos disponibles, luego regresamos por donde vinimos. :-)

Finalmente, recuerden LOAD_NAME, ese es otro tipo que contribuye aquí.


Es fácil ver que, cuando se trata de nuestra entrada, Python generalmente tiene que pasar por obstáculos para encontrar la Cfunción adecuada para hacer el trabajo. No tiene la cortesía de llamarlo inmediatamente porque es dinámico, alguien podría enmascararse list( y mucha gente lo hace ) y hay que tomar otro camino.

Aquí es donde list()se pierde mucho: Python debe explorar para descubrir qué diablos debe hacer.

La sintaxis literal, por otra parte, significa exactamente una cosa; no se puede cambiar y siempre se comporta de una manera predeterminada.

Nota al pie: Todos los nombres de funciones están sujetos a cambios de una versión a otra. El punto sigue vigente y muy probablemente seguirá vigente en versiones futuras: es la búsqueda dinámica la que ralentiza las cosas.