2016-05-21 8 views
26

Ich habe eine Python 3.6 Alpha-Build aus dem Python Github-Repository heruntergeladen, und eine meiner beliebtesten neuen Funktionen ist literale String-Formatierung. Es kann wie so verwendet werden:Warum sind literal formatierte Zeichenfolgen in Python 3.6 alpha so langsam? (jetzt in 3.6 stabil behoben)

>>> x = 2 
>>> f"x is {x}" 
"x is 2" 

Dies scheint die gleichen wie mit der format Funktion auf einer str Instanz zu tun. Eine Sache, die ich bemerkt habe, ist jedoch, dass diese literale String-Formatierung tatsächlich sehr langsam ist, verglichen mit dem Aufruf von format. Hier ist, was timeit sagt über jede Methode:

>>> x = 2 
>>> timeit.timeit(lambda: f"X is {x}") 
0.8658502227130764 
>>> timeit.timeit(lambda: "X is {}".format(x)) 
0.5500578542015617 

Wenn ich eine Zeichenfolge als timeit ‚s Argument verwenden, werden meine Ergebnisse immer noch das Muster zeigt:

>>> timeit.timeit('x = 2; f"X is {x}"') 
0.5786435347381484 
>>> timeit.timeit('x = 2; "X is {}".format(x)') 
0.4145195760771685 

Wie Sie sehen können, mit format dauert fast die Hälfte der Zeit. Ich würde erwarten, dass die literale Methode schneller ist, weil weniger Syntax involviert ist. Was passiert hinter den Kulissen, wodurch die literale Methode so viel langsamer wird?

+0

f-Strings sind dynamisch, daher muss der String in jeder Schleife generiert werden; während die Format-Zeichenfolge ein Literal ist, das erstellt wird, bevor der Code ausgeführt wird, wenn er in Bytecode konvertiert wird. –

+0

@AlexHall Vielleicht hat dies mit der Tatsache zu tun, dass 'x' einer lokalen Variablen zugewiesen wird, wenn sie an die' format' Methode übergeben wird, aber in 'globals' durch' f "..." 'gefunden werden muss Syntax. – schwobaseggl

+1

@AlexHall: Dies ist kein Fehler. Es gibt einfach eine andere Implementierung unter der Haube, da der Formatstring zur Kompilierzeit geparst werden muss, während 'str.format()' die Slots nach * runtime * analysiert. –

Antwort

23

Hinweis: Diese Antwort wurde für die Python 3.6 Alpha-Versionen geschrieben. A new opcode added to 3.6.0b1 verbesserte die f-Saitenleistung signifikant.


f"..." Die Syntax wird eine str.join() Operation an den Literal Teilen um die {...} Ausdrücke effektiv umgewandelt, und die Ergebnisse der Ausdrücke selbst gingen durch die object.__format__() Methode (vorbei jede :.. Formatangabe in). Sie können das sehen, wenn Demontage:

>>> 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 

Beachten Sie die BUILD_LIST und LOAD_ATTR .. (join) op-Codes in diesem Ergebnis. Der neue FORMAT_VALUE übernimmt den Anfang des Stapels plus einen Formatwert (der zur Kompilierungszeit herausgefiltert wurde), um diese in einem object.__format__() Aufruf zu kombinieren.

So Ihr Beispiel f"X is {x}", wird übersetzt:

''.join(["X is ", x.__format__('')]) 

Beachten Sie, dass diese Python erfordert eine Liste Objekt zu erstellen, und rufen Sie die str.join() Methode.

Der str.format() Aufruf ist auch ein Methodenaufruf, und nach dem Parsen gibt es noch einen Aufruf an x.__format__('') beteiligt, aber entscheidend ist, gibt es keine Listenerstellung hier beteiligt. Es ist dieser Unterschied, der die Methode str.format() schneller macht.

Beachten Sie, dass Python 3.6 nur als Alpha-Build veröffentlicht wurde; Diese Implementierung kann sich immer noch leicht ändern. Unter PEP 494 – Python 3.6 Release Schedule für die Zeittabelle sowie Python issue #27078 (als Antwort auf diese Frage geöffnet) finden Sie eine Erläuterung zur weiteren Verbesserung der Leistung formatierter Zeichenfolgenliterale.

+0

Wirklich nette Erklärung, danke! Ich hatte keine Ahnung, dass es auch eine "__format__" magische Methode gab. –

+0

Warum wird es zu ''' erweitert .join ([...])' und nicht String-Verkettung? –

+3

@AlexHall: weil String-Verkettung O (N^2) Leistungsmerkmale hat. A + B + C muss zuerst eine Zeichenfolge für A + B erstellen und dann das Ergebnis zusammen mit C in eine neue Zeichenfolge kopieren. –

16

Vor 3.6 Beta 1 wurde die Formatzeichenkette f'x is {x}' in das Äquivalent ''.join(['x is ', x.__format__('')]) kompiliert.Der resultierende Code ineffizient war aus mehreren Gründen:

  1. es eine Folge von String-Fragmente gebaut ...
  2. ... und diese Sequenz war eine Liste, kein Tupel! (Es ist etwas schneller, Tupel als Listen zu konstruieren).
  3. geschoben einen leeren String auf dem Stapel
  4. die join Methode auf dem leeren String
  5. es __format__ auf sogar nackte Unicode Objekten aufgerufen nachgeschlagen, für die die __format__('') immer self oder ganzzahlige Objekte zurückkehren würde, für Die __format__('') als Argument zurückgegeben str(self).
  6. __format__ Methode ist nicht geschlitzt.

jedoch für eine komplexere und längere Zeichenfolge, die wörtliche formatiert Strings noch wäre schneller gewesen als die entsprechende '...'.format(...) Ruf, weil für letztere die Zeichenfolge jedes Mal interpretiert wird der String formatiert ist.


diese Frage war der wichtigste Motivator für issue 27078 für einen neuen Python-Bytecode Opcode bittet um eine Zeichenfolge aus Fragmenten verketten. Serhiy Storchaka implementierte diesen neuen Opcode und fasste ihn in CPython zusammen, so dass er seit der Beta 1-Version (und somit in Python 3.6.0 final) in Python 3.6 verfügbar war.

Als Ergebnis werden die literal formatierten Strings viel schneller als string.format. Sie sind auch oft viel schneller als die alten Stil Formatierung in Python 3.6, wenn Sie gerade Interpolations-str oder int Objekte:

>>> 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}' kompiliert nun

>>> 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 

Die neue BUILD_STRING , zusammen mit einer Optimierung in FORMAT_VALUE Code vollständig beseitigt ersten 5 der 6 Ursachen von Ineffizienz. Die __format__ Methode ist immer noch nicht geschlitzt, erfordert also ein Dictionary Lookup auf der Klasse und damit Aufruf ist langsamer als Aufruf __str__, aber ein Anruf kann nun vollständig vermieden werden in den üblichen Fällen der Formatierung int oder str Instanzen (keine Unterklassen !) ohne Formatierungsangaben.

0

Nur eine Aktualisierung fest, dass dies in Python3.6 Release gelöst scheint.

>>> 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 
+4

Ja, wie bereits in Anttis Antwort beschrieben, die detailliert erklärt, was der neue Opcode tut. –

Verwandte Themen