example python python-2.7 performance python-internals

python - example - ¿Por qué float() es más rápido que int()?



python 3 cprofile (3)

Esta no es una respuesta completa, solo algunos datos y observaciones.

Resultados de perfiles de x86-64 Arch Linux, Python 2.7.14, en un Skylake i7-6700k de 3.9GHz con Linux 4.15.8-1-ARCH. float : 0.0854 usec por ciclo. int : 0.196 usec por ciclo. (Entonces, un factor de 2)

flotador

$ perf record python2.7 -m timeit ''float("1")'' 10000000 loops, best of 3: 0.0854 usec per loop Samples: 14K of event ''cycles:uppp'', Event count (approx.): 13685905532 Overhead Command Shared Object Symbol 29.73% python2.7 libpython2.7.so.1.0 [.] PyEval_EvalFrameEx 8.54% python2.7 libpython2.7.so.1.0 [.] _Py_dg_strtod 8.30% python2.7 libpython2.7.so.1.0 [.] vgetargskeywords 5.81% python2.7 libpython2.7.so.1.0 [.] lookdict_string.lto_priv.1492 4.79% python2.7 libpython2.7.so.1.0 [.] PyFloat_FromString 4.67% python2.7 libpython2.7.so.1.0 [.] tupledealloc.lto_priv.335 4.16% python2.7 libpython2.7.so.1.0 [.] float_new.lto_priv.219 3.93% python2.7 libpython2.7.so.1.0 [.] _PyOS_ascii_strtod 3.54% python2.7 libc-2.26.so [.] __strchr_avx2 3.34% python2.7 libpython2.7.so.1.0 [.] PyOS_string_to_double 3.21% python2.7 libpython2.7.so.1.0 [.] PyTuple_New 3.05% python2.7 libpython2.7.so.1.0 [.] type_call.lto_priv.51 2.69% python2.7 libpython2.7.so.1.0 [.] PyObject_Call 2.15% python2.7 libpython2.7.so.1.0 [.] PyArg_ParseTupleAndKeywords 1.88% python2.7 itertools.so [.] _init 1.78% python2.7 libpython2.7.so.1.0 [.] _Py_set_387controlword 1.19% python2.7 libpython2.7.so.1.0 [.] _Py_get_387controlword 1.10% python2.7 libpython2.7.so.1.0 [.] vgetargskeywords.cold.59 1.07% python2.7 libpython2.7.so.1.0 [.] PyType_IsSubtype 1.07% python2.7 libc-2.26.so [.] __memset_avx2_unaligned_erms ...

IDK por qué diablos Python está jugando con la palabra de control x87, pero sí, la pequeña función _Py_get_387controlword realmente ejecuta fnstcw WORD PTR [rsp+0x6] y luego la vuelve a cargar en eax como un valor de retorno entero con movzx , pero probablemente gasta más de es hora de escribir y verificar el stack canary de -fstack-protector-strong .

Es extraño porque _Py_dg_strtod usa SSE2 ( cvtsi2sd xmm1,rsi ) para FP matemática, no x87. (La parte caliente con esta entrada es en su mayoría entera, pero hay mulsd y divsd allí). El código x86-64 normalmente solo usa x87 para el long double (flotador de 80 bits). dg_strtod significa dg_strtod de David Gay para duplicar. Interesante entrada de blog sobre cómo funciona bajo el capó .

Tenga en cuenta que esta función solo toma el 9% del tiempo de ejecución total. El resto es básicamente una sobrecarga de intérprete, en comparación con un ciclo C que llamó strtod en un bucle y arrojó el resultado.

En t

$ perf record python2.7 -m timeit ''int("1")'' 10000000 loops, best of 3: 0.196 usec per loop $ perf report -Mintel Samples: 32K of event ''cycles:uppp'', Event count (approx.): 31257616633 Overhead Command Shared Object Symbol 29.00% python2.7 libpython2.7.so.1.0 [.] PyString_FromFormatV 13.11% python2.7 libpython2.7.so.1.0 [.] PyEval_EvalFrameEx 5.49% python2.7 libc-2.26.so [.] __strlen_avx2 3.87% python2.7 libpython2.7.so.1.0 [.] vgetargskeywords 3.68% python2.7 libpython2.7.so.1.0 [.] PyNumber_Int 3.10% python2.7 libpython2.7.so.1.0 [.] PyInt_FromString 2.75% python2.7 libpython2.7.so.1.0 [.] PyErr_Restore 2.68% python2.7 libc-2.26.so [.] __strchr_avx2 2.41% python2.7 libpython2.7.so.1.0 [.] tupledealloc.lto_priv.335 2.10% python2.7 libpython2.7.so.1.0 [.] PyObject_Call 2.00% python2.7 libpython2.7.so.1.0 [.] PyOS_strtoul 1.93% python2.7 libpython2.7.so.1.0 [.] lookdict_string.lto_priv.1492 1.87% python2.7 libpython2.7.so.1.0 [.] _PyObject_GenericGetAttrWithDict 1.73% python2.7 libpython2.7.so.1.0 [.] PyString_FromStringAndSize 1.71% python2.7 libc-2.26.so [.] __memmove_avx_unaligned_erms 1.67% python2.7 libpython2.7.so.1.0 [.] PyTuple_New 1.63% python2.7 libpython2.7.so.1.0 [.] PyObject_Malloc 1.48% python2.7 libpython2.7.so.1.0 [.] int_new.lto_priv.68 1.45% python2.7 libpython2.7.so.1.0 [.] PyErr_Format 1.45% python2.7 libpython2.7.so.1.0 [.] PyObject_Realloc 1.37% python2.7 libpython2.7.so.1.0 [.] type_call.lto_priv.51 1.30% python2.7 libpython2.7.so.1.0 [.] PyOS_strtol 1.23% python2.7 libpython2.7.so.1.0 [.] _PyString_Resize 1.16% python2.7 libc-2.26.so [.] __ctype_b_loc 1.11% python2.7 libpython2.7.so.1.0 [.] _PyType_Lookup 1.06% python2.7 libpython2.7.so.1.0 [.] PyString_AsString 1.04% python2.7 libpython2.7.so.1.0 [.] PyArg_ParseTupleAndKeywords 1.02% python2.7 libpython2.7.so.1.0 [.] PyObject_Free 0.93% python2.7 libpython2.7.so.1.0 [.] PyInt_FromLong 0.90% python2.7 libpython2.7.so.1.0 [.] PyObject_GetAttr 0.52% python2.7 libc-2.26.so [.] __memset_avx2_unaligned_erms 0.52% python2.7 libpython2.7.so.1.0 [.] vgetargskeywords.cold.59 0.48% python2.7 itertools.so [.] _init ...

Tenga en cuenta que PyEval_EvalFrameEx toma un 13% del tiempo total para int , frente al 30% del total de float . Eso es aproximadamente el mismo tiempo absoluto, y PyString_FromFormatV toma el doble de tiempo. Además de más funciones que toman más pequeños fragmentos de tiempo.

No he descubierto qué hace PyInt_FromString , o en qué está gastando su tiempo. 7% de sus recuentos de ciclos se cargan a una movdqu xmm0, [rsi] cerca del inicio; es decir, cargar un arg de 16 bytes que se pasó por referencia (como la 2ª función arg). Esto puede obtener más conteos de lo que merece si lo que almacenó esa memoria fue lento para producirlo. (Consulte esta sección de preguntas y respuestas para obtener más información acerca de cómo los recuentos de ciclo se cargan a las instrucciones de ejecución de CPU Intel en las que se realizan muchos trabajos diferentes en cada ciclo). O tal vez se obtienen recuentos de un puesto de reenvío de tiendas si esa memoria escrito recientemente con tiendas más estrechas separadas.

Es sorprendente que el strlen tome tanto tiempo . Al mirar el perfil de instrucciones dentro de él, está obteniendo cadenas cortas, pero no cadenas de 1 byte exclusivamente. Parece una mezcla de len < 32 bytes y 64 < len >= 32 bytes. Puede ser interesante establecer un punto de interrupción en gdb y ver qué argumentos son comunes.

La versión flotante tiene un strchr (¿quizás buscando un punto decimal?), Pero no strlen de nada. Es sorprendente que la versión int tenga que strlen un strlen dentro del ciclo.

La función real PyOS_strtoul tarda el 2% del tiempo total, se ejecuta desde PyInt_FromString (3% del tiempo total). Estos son tiempos "propios", sin incluir a sus hijos, por lo que asignar memoria y decidir sobre la base numérica lleva más tiempo que analizar un solo dígito.

Un ciclo equivalente en C correría ~ 50x más rápido (o tal vez 20 veces si somos generosos), llamando a strtoul en una cadena constante y descartando el resultado.

int con base explícita

Por alguna razón, esto es tan rápido como float .

$ perf record python2.7 -m timeit ''int("1",10)'' 10000000 loops, best of 3: 0.0894 usec per loop $ perf report -Mintel Samples: 14K of event ''cycles:uppp'', Event count (approx.): 14289699408 Overhead Command Shared Object Symbol 30.84% python2.7 libpython2.7.so.1.0 [.] PyEval_EvalFrameEx 12.56% python2.7 libpython2.7.so.1.0 [.] vgetargskeywords 6.70% python2.7 libpython2.7.so.1.0 [.] PyInt_FromString 5.19% python2.7 libpython2.7.so.1.0 [.] tupledealloc.lto_priv.335 5.17% python2.7 libpython2.7.so.1.0 [.] int_new.lto_priv.68 4.12% python2.7 libpython2.7.so.1.0 [.] lookdict_string.lto_priv.1492 4.08% python2.7 libpython2.7.so.1.0 [.] PyOS_strtoul 3.78% python2.7 libc-2.26.so [.] __strchr_avx2 3.29% python2.7 libpython2.7.so.1.0 [.] type_call.lto_priv.51 3.26% python2.7 libpython2.7.so.1.0 [.] PyTuple_New 3.09% python2.7 libpython2.7.so.1.0 [.] PyOS_strtol 3.06% python2.7 libpython2.7.so.1.0 [.] PyObject_Call 2.49% python2.7 libpython2.7.so.1.0 [.] PyArg_ParseTupleAndKeywords 2.01% python2.7 libpython2.7.so.1.0 [.] PyType_IsSubtype 1.65% python2.7 libc-2.26.so [.] __strlen_avx2 1.52% python2.7 libpython2.7.so.1.0 [.] object_init.lto_priv.86 1.19% python2.7 libpython2.7.so.1.0 [.] vgetargskeywords.cold.59 1.03% python2.7 libpython2.7.so.1.0 [.] PyInt_AsLong 1.00% python2.7 libpython2.7.so.1.0 [.] PyString_Size 0.99% python2.7 libpython2.7.so.1.0 [.] PyObject_GC_UnTrack 0.87% python2.7 libc-2.26.so [.] __ctype_b_loc 0.85% python2.7 libc-2.26.so [.] __memset_avx2_unaligned_erms 0.47% python2.7 itertools.so [.] _init

El perfil por función parece bastante similar a la versión float .

Experimentando con algún código y haciendo algunas microbenchmarks Acabo de descubrir que usar la función float en una cadena que contiene un número entero es un factor 2 más rápido que usar int en la misma cadena.

>>> python -m timeit int(''1'') 1000000 loops, best of 3: 0.548 usec per loop >>> python -m timeit float(''1'') 1000000 loops, best of 3: 0.273 usec per loop

Se vuelve aún más extraño cuando se prueba int(float(''1'')) cuyo tiempo de ejecución es más corto que el desnudo int(''1'') .

>>> python -m timeit int(float(''1'')) 1000000 loops, best of 3: 0.457 usec per loop

Probé el código en Windows 7 con cPython 2.7.6 y Linux Mint 16 con cPython 2.7.6.

Tengo que agregar que solo Python 2 se ve afectado, Python 3 muestra una diferencia mucho menor (no notable) entre los tiempos de ejecución.

Sé que la información que obtengo de tales microbenchmarks es fácil de usar de forma incorrecta, pero tengo curiosidad de por qué hay tanta diferencia en el tiempo de ejecución de las funciones.

Traté de encontrar las implementaciones de int y float pero no puedo encontrarlo en las fuentes.


int() tiene que dar cuenta de más tipos posibles para convertir desde que float() tiene que hacerlo. Cuando pasa un único objeto a int() y no es un número entero, se prueban varias cosas:

  1. si ya es un número entero, úselo directamente
  2. si el objeto implementa el método __int__ , llámalo y usa el resultado
  3. si el objeto es una subclase de int derivada de C, alcance y convierta el valor entero de C en la estructura a un objeto int() .
  4. si el objeto implementa el método __trunc__ , llámalo y usa el resultado
  5. si el objeto es una cadena, conviértalo en un entero con la base establecida en 10.

Ninguna de estas pruebas se ejecuta cuando pasa un argumento base, el código luego salta directamente a la conversión de una cadena a un int, con la base seleccionada. Eso es porque no hay otros tipos aceptados, no cuando se da una base.

Como resultado, cuando pasas en una base, crear un número entero a partir de una cadena es mucho más rápido:

$ bin/python -m timeit "int(''1'')" 1000000 loops, best of 3: 0.469 usec per loop $ bin/python -m timeit "int(''1'', 10)" 1000000 loops, best of 3: 0.277 usec per loop $ bin/python -m timeit "float(''1'')" 1000000 loops, best of 3: 0.206 usec per loop

Cuando pasa una cadena a float() , la primera prueba que se realiza es ver si el argumento es un objeto de cadena (y no una subclase), en cuyo punto se está analizando. No hay necesidad de probar otros tipos.

Entonces, la llamada int(''1'') realiza algunas pruebas más que int(''1'', 10) o float(''1'') . De esas pruebas, las pruebas 1, 2 y 3 son bastante rápidas; solo son controles de puntero. Pero la cuarta prueba usa el equivalente en C de getattr(obj, ''__trunc__'') , que es relativamente caro . Esto tiene que probar la instancia, y el MRO completo de la cadena, y no hay caché, y al final levanta un AttributeError() , formateando un mensaje de error que nadie verá nunca. Todo el trabajo es bastante inútil aquí.

En Python 3, esa llamada a getattr() ha sido reemplazada por un código que es mucho más rápido. Esto se debe a que en Python 3 no es necesario contar las clases antiguas para que el atributo pueda buscarse directamente en el tipo de la instancia (la clase, el resultado del type(instance) ) y las búsquedas de atributos de clase en el MRO están en caché en este punto. No es necesario crear excepciones.

float() objetos float() implementan el método __int__ , por lo que int(float(''1'')) es más rápido; nunca golpeó la prueba de atributo __trunc__ en el paso 4 ya que el paso 2 produjo el resultado en su lugar.

Si quería ver el código C, para Python 2, int_new() mire el método int_new() . Después de analizar los argumentos, el código esencialmente hace esto:

if (base == -909) // no base argument given, the default is -909 return PyNumber_Int(x); // parse an integer from x, an arbitrary type. if (PyString_Check(x)) { // do some error handling; there is a base, so parse the string with the base return PyInt_FromString(string, NULL, base); }

El caso sin base llama a la función PyNumber_Int() , que hace esto:

if (PyInt_CheckExact(o)) { // 1. it''s an integer already // ... } m = o->ob_type->tp_as_number; if (m && m->nb_int) { /* This should include subclasses of int */ // 2. it has an __int__ method, return the result // ... } if (PyInt_Check(o)) { /* An int subclass without nb_int */ // 3. it''s an int subclass, extract the value // ... } trunc_func = PyObject_GetAttr(o, trunc_name); if (trunc_func) { // 4. it has a __trunc__ method, call it and process the result // ... } if (PyString_Check(o)) // 5. it''s a string, lets parse! return int_from_string(PyString_AS_STRING(o), PyString_GET_SIZE(o));

donde int_from_string() es esencialmente un contenedor para PyInt_FromString(string, length, 10) , por lo que se analiza la cadena con la base 10.

En Python 3, se eliminó intobject , dejando solo longobject , renombrado a int() en el lado de Python. En la misma línea, unicode ha reemplazado a str . Así que ahora nos fijamos en long_new() , y la prueba de una cadena se realiza con PyUnicode_Check() lugar de PyString_Check() :

if (obase == NULL) return PyNumber_Long(x); // bounds checks on the obase argument, storing a conversion in base if (PyUnicode_Check(x)) return PyLong_FromUnicodeObject(x, (int)base);

Así que de nuevo cuando no se establece ninguna base, tenemos que mirar PyNumber_Long() , que ejecuta:

if (PyLong_CheckExact(o)) { // 1. it''s an integer already // ... } m = o->ob_type->tp_as_number; if (m && m->nb_int) { /* This should include subclasses of int */ // 2. it has an __int__ method // ... } trunc_func = _PyObject_LookupSpecial(o, &PyId___trunc__); if (trunc_func) { // 3. it has a __trunc__ method // ... } if (PyUnicode_Check(o)) // 5. it''s a string return PyLong_FromUnicodeObject(o, 10);

Observe la llamada _PyObject_LookupSpecial() , esta es la implementación de búsqueda de método especial ; eventualmente usa _PyType_Lookup() , que usa un caché; dado que no str.__trunc__ método str.__trunc__ , la memoria caché devolverá nulo para siempre después de la primera exploración MRO. Este método tampoco genera una excepción, solo devuelve el método solicitado o un valor nulo.

La forma en que float() maneja las cadenas no cambia entre Python 2 y 3, por lo que solo necesita mirar la función float_new() Python 2 , que para las cadenas es bastante sencilla:

// test for subclass and retrieve the single x argument /* If it''s a string, but not a string subclass, use PyFloat_FromString. */ if (PyString_CheckExact(x)) return PyFloat_FromString(x, NULL); return PyNumber_Float(x);

Entonces, para los objetos de cuerda, PyNumber_Float() directamente al análisis, de lo contrario usamos PyNumber_Float() para buscar objetos float reales, o cosas con un método __float__ , o para subclases de cuerda.

Esto revela una posible optimización: si int() probara primero PyString_CheckExact() antes de todas esas otras pruebas de tipo, sería tan rápido como float() cuando se trata de cadenas. PyString_CheckExact() descarta una subclase de cadena que tiene un método __int__ o __trunc__ , por lo que es una buena primera prueba.

Para abordar otras respuestas que achacan esto al análisis base (buscando un prefijo 0b , 0o , 0 o 0x , caso insensible), la llamada int() predeterminada con un solo argumento de cadena busca una base , la base está codificada a 10 Es un error pasar una cadena con un prefijo en ese caso:

>>> int(''0x1'') Traceback (most recent call last): File "<stdin>", line 1, in <module> ValueError: invalid literal for int() with base 10: ''0x1''

El análisis del prefijo base solo se realiza si establece explícitamente el segundo argumento en 0 :

>>> int(''0x1'', 0) 1

Como no se realiza ninguna prueba para __trunc__ el caso de análisis de prefijo base=0 es tan rápido como establecer la base explícitamente para cualquier otro valor soportado:

$ python2.7 -m timeit "int(''1'')" 1000000 loops, best of 3: 0.472 usec per loop $ python2.7 -m timeit "int(''1'', 10)" 1000000 loops, best of 3: 0.268 usec per loop $ python2.7 bin/python -m timeit "int(''1'', 0)" 1000000 loops, best of 3: 0.271 usec per loop $ python2.7 bin/python -m timeit "int(''0x1'', 0)" 1000000 loops, best of 3: 0.261 usec per loop


int tiene muchas bases.

*, 0 *, 0x *, 0b *, 0o * y puede ser largo, toma tiempo determinar la base y otras cosas

si la base está configurada, ahorra mucho tiempo

python -m timeit "int(''1'',10)" 1000000 loops, best of 3: 0.252 usec per loop python -m timeit "int(''1'')" 1000000 loops, best of 3: 0.594 usec per loop

como @Martijn Pieters menciona el código Object/intobject.c(int_new) y Object/floatobject.c(float_new)