xf1 - ¿Por qué las cadenas con formato literal son tan lentas en Python 3.6 alfa?(ahora fijado en 3.6 estable)
u ''/ xf3 (3)
He descargado una compilación alfa de Python 3.6 del repositorio de Python Github, y una de mis nuevas características favoritas es el formato de cadena literal. Puede ser utilizado como tal:
>>> x = 2
>>> f"x is {x}"
"x is 2"
Esto parece hacer lo mismo que usar la función de format
en una instancia de str
. Sin embargo, una cosa que he notado es que este formato de cadena literal es en realidad muy lento en comparación con solo el format
llamada. timeit
es lo que dice timeit
acerca de cada método:
>>> x = 2
>>> timeit.timeit(lambda: f"X is {x}")
0.8658502227130764
>>> timeit.timeit(lambda: "X is {}".format(x))
0.5500578542015617
Si uso una cadena como argumento de timeit
, mis resultados siguen mostrando el patrón:
>>> timeit.timeit(''x = 2; f"X is {x}"'')
0.5786435347381484
>>> timeit.timeit(''x = 2; "X is {}".format(x)'')
0.4145195760771685
Como puede ver, usar el format
lleva casi la mitad del tiempo. Espero que el método literal sea más rápido porque hay menos sintaxis involucrada. ¿Qué sucede detrás de escena que hace que el método literal sea mucho más lento?
Antes de 3.6 beta 1, la cadena de formato f''x is {x}''
se compiló al equivalente de ''''.join([''x is '', x.__format__('''')])
. El código resultante fue ineficiente por varias razones:
- construyó una secuencia de fragmentos de cuerda ...
- ... y esta secuencia era una lista, no una tupla! (Es un poco más rápido construir tuplas que listas).
- empujó una cadena vacía en la pila
- Buscó el método de
join
en la cadena vacía. - invocó
__format__
incluso en objetos Unicode simples, para los cuales__format__('''')
siempre devolveríaself
, u objetos enteros, para los cuales__format__('''')
como el argumento devolvióstr(self)
. -
__format__
método de__format__
no es ranurado
Sin embargo, para una cadena más compleja y más larga, las cadenas con formato literal aún habrían sido más rápidas que la llamada ''...''.format(...)
, porque para esta última la cadena se interpreta cada vez que se formatea la cadena. .
Esta pregunta fue el principal motivador para el problema 27078, que solicitó un nuevo código de operación de bytecode de Python para concatenar una cadena de fragmentos. Serhiy Storchaka implementó este nuevo código de operación y lo fusionó en CPython para que esté disponible en Python 3.6 desde la versión beta 1 (y, por lo tanto, en la final de Python 3.6.0).
Como resultado, las cadenas con formato literal serán mucho más rápidas que string.format
. A menudo, también son mucho más rápidos que el formato antiguo en Python 3.6, si solo está interpolando objetos str
o int
:
>>> timeit.timeit("x = 2; ''X is {}''.format(x)")
0.32464265200542286
>>> timeit.timeit("x = 2; ''X is %s'' % x")
0.2260766440012958
>>> timeit.timeit("x = 2; f''X is {x}''")
0.14437875000294298
f''X is {x}''
ahora compila a
>>> dis.dis("f''X is {x}''")
1 0 LOAD_CONST 0 (''X is '')
2 LOAD_NAME 0 (x)
4 FORMAT_VALUE 0
6 BUILD_STRING 2
8 RETURN_VALUE
El nuevo BUILD_STRING
, junto con una optimización en el código FORMAT_VALUE
elimina completamente las primeras 5 de las 6 fuentes de ineficiencia. El método __format__
aún no tiene ranuras, por lo que requiere una búsqueda de diccionario en la clase y, por lo tanto, llamarla es necesariamente más lento que llamar a __str__
, pero ahora se puede evitar completamente una llamada en los casos comunes de formato de instancias int
o str
(no subclases !) sin especificadores de formato.
Solo una actualización que indica que esto parece resolverse en la versión Python3.6.
>>> import dis
>>> dis.dis(compile(''f"X is {x}"'', '''', ''exec''))
1 0 LOAD_CONST 0 (''X is '')
2 LOAD_NAME 0 (x)
4 FORMAT_VALUE 0
6 BUILD_STRING 2
8 POP_TOP
10 LOAD_CONST 1 (None)
12 RETURN_VALUE
>>> dis.dis(compile(''"X is {}".format(x)'', '''', ''exec''))
1 0 LOAD_CONST 0 (''X is {}'')
2 LOAD_ATTR 0 (format)
4 LOAD_NAME 1 (x)
6 CALL_FUNCTION 1
8 POP_TOP
10 LOAD_CONST 1 (None)
12 RETURN_VALUE
Nota : esta respuesta se escribió para las versiones alfa de Python 3.6. Un nuevo código de operación agregado a 3.6.0b1 mejoró significativamente el rendimiento de f-string.
La sintaxis f"..."
se convierte efectivamente en una operación str.join()
en las partes de la cadena literal alrededor de las expresiones {...}
, y los resultados de las propias expresiones pasaron a través del método object.__format__()
pasando cualquier :..
especificación de formato en). Puedes ver esto al desmontar:
>>> import dis
>>> dis.dis(compile(''f"X is {x}"'', '''', ''exec''))
1 0 LOAD_CONST 0 ('''')
3 LOAD_ATTR 0 (join)
6 LOAD_CONST 1 (''X is '')
9 LOAD_NAME 1 (x)
12 FORMAT_VALUE 0
15 BUILD_LIST 2
18 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
21 POP_TOP
22 LOAD_CONST 2 (None)
25 RETURN_VALUE
>>> dis.dis(compile(''"X is {}".format(x)'', '''', ''exec''))
1 0 LOAD_CONST 0 (''X is {}'')
3 LOAD_ATTR 0 (format)
6 LOAD_NAME 1 (x)
9 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
12 POP_TOP
13 LOAD_CONST 1 (None)
16 RETURN_VALUE
Tenga en cuenta los BUILD_LIST
LOAD_ATTR .. (join)
BUILD_LIST
y LOAD_ATTR .. (join)
en ese resultado. El nuevo FORMAT_VALUE
toma la parte superior de la pila más un valor de formato (analizado en tiempo de compilación) para combinarlos en una llamada de object.__format__()
.
Entonces su ejemplo, f"X is {x}"
, se traduce a:
''''.join(["X is ", x.__format__('''')])
Tenga en cuenta que esto requiere que Python cree un objeto de lista y llame al método str.join()
.
La llamada str.format()
también es una llamada de método, y después del análisis todavía hay una llamada al x.__format__('''')
involucrada, pero fundamentalmente, no hay creación de lista involucrada aquí. Es esta diferencia la que hace que el método str.format()
más rápido.
Tenga en cuenta que Python 3.6 solo se ha lanzado como una compilación alfa; Esta implementación todavía puede cambiar fácilmente. Consulte el PEP 494 - Programación de lanzamiento de Python 3.6 para ver la tabla de tiempo, así como el problema de Python # 27078 (abierto en respuesta a esta pregunta) para una discusión sobre cómo mejorar aún más el rendimiento de los literales de cadena con formato.