41 votes

Pourquoi les chaînes littérales formatées (f-strings) étaient-elles si lentes dans Python 3.6 alpha ? (maintenant corrigé dans la version 3.6 stable)

J'ai téléchargé une version alpha de Python 3.6 depuis le dépôt Github de Python, et l'une de mes nouvelles fonctionnalités préférées est le formatage des chaînes littérales. Il peut être utilisé comme suit :

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

Cela semble faire la même chose que d'utiliser la fonction format sur une str instance. Cependant, j'ai remarqué que ce formatage littéral de la chaîne de caractères est en fait très lent par rapport à un simple appel à la fonction format . Voici ce que timeit dit à propos de chaque méthode :

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

Si j'utilise une chaîne de caractères comme timeit mais mes résultats montrent toujours le même schéma :

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

Comme vous pouvez le constater, l'utilisation de format prend presque la moitié du temps. Je m'attendrais à ce que la méthode littérale soit plus rapide, car elle fait appel à moins de syntaxe. Que se passe-t-il en coulisses pour que la méthode littérale soit si lente ?

2 votes

Les chaînes f-strings sont dynamiques, de sorte que la chaîne doit être générée à chaque boucle, tandis que la chaîne format est un littéral créé avant l'exécution du code, lors de sa conversion en bytecode.

0 votes

@AlexHall Peut-être que cela a à voir avec le fait que x est assigné à une variable locale lorsqu'il est transmis à la fonction format mais doit être trouvée dans la méthode globals par le f"..." la syntaxe.

2 votes

@AlexHall : ce n'est pas un bug. Il y a simplement une implémentation différente sous le capot, car la chaîne de format doit être analysée au moment de la compilation, alors que l'implémentation de la chaîne de format n'a pas lieu. str.format() analyse les slots à temps de fonctionnement .

45voto

Martijn Pieters Points 271458

Nota : Cette réponse a été écrite pour les versions alpha de Python 3.6. A nouvel opcode ajouté à la 3.6.0b1 a amélioré de manière significative les performances de la corde f.


En f"..." est effectivement convertie en une syntaxe str.join() sur les parties littérales de la chaîne de caractères autour du {...} les expressions, et les résultats des expressions elles-mêmes sont passés par le biais de la object.__format__() (en passant tout :.. spécification du format en). Vous pouvez le constater lors du désassemblage :

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

Notez le BUILD_LIST y LOAD_ATTR .. (join) op-codes dans ce résultat. Le nouveau FORMAT_VALUE prend le haut de la pile plus une valeur de format (analysée au moment de la compilation) pour les combiner dans un fichier de type object.__format__() appeler.

Donc votre exemple, f"X is {x}" est traduit par :

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

Notez que cela nécessite que Python crée un objet liste, et appelle la fonction str.join() méthode.

En str.format() est également un appel de méthode, et après l'analyse syntaxique, il y a toujours un appel à la méthode x.__format__('') mais, surtout, il n'y a pas de création de listes impliqué ici. C'est cette différence qui rend le str.format() plus rapidement.

Notez que Python 3.6 n'a été publié qu'en tant que build alpha ; cette implémentation peut encore facilement changer. Voir PEP 494 - Calendrier de sortie de Python 3.6 pour l'horaire, ainsi que Problème Python #27078 (ouvert en réponse à cette question) pour une discussion sur la manière d'améliorer encore les performances des chaînes de caractères formatées.

0 votes

Très bonne explication, merci ! Je ne savais pas qu'il existait un __format__ méthode magique non plus.

0 votes

Pourquoi est-il élargi à ''.join([...]) et non la concaténation de chaînes de caractères ?

3 votes

@AlexHall : parce que la concaténation de chaînes a des caractéristiques de performance O(N^2). A + B + C doit d'abord créer une chaîne pour A + B, puis copier le résultat avec C dans une nouvelle chaîne.

38voto

Antti Haapala Points 11542

Avant la version 3.6 beta 1, la chaîne de format f'x is {x}' a été compilé en l'équivalent de ''.join(['x is ', x.__format__('')]) . Le bytecode résultant était inefficace pour plusieurs raisons :

  1. il a construit une séquence de fragments de chaîne...
  2. ... et cette séquence était une liste, pas un tuple ! (il est légèrement plus rapide de construire des tuples que des listes).
  3. il a poussé une chaîne vide sur la pile
  4. il a regardé le join sur la chaîne vide
  5. il a invoqué __format__ sur des objets Unicode, même nus, pour lesquels la fonction __format__('') retournerait toujours self ou des objets entiers, pour lesquels __format__('') comme l'argument retourné str(self) .
  6. __format__ n'est pas fendue.

Toutefois, pour une chaîne plus complexe et plus longue, les chaînes formatées littéralement auraient toujours été plus rapides que les chaînes correspondantes. '...'.format(...) car pour ce dernier, la chaîne est interprétée à chaque fois que la chaîne est formatée.


Cette question a été la principale motivation pour numéro 27078 demandant un nouvel opcode du bytecode Python pour les fragments de chaîne de caractères dans une chaîne (l'opcode reçoit un opérande - le nombre de fragments sur la pile ; les fragments sont poussés sur la pile dans l'ordre d'apparition, c'est-à-dire que la dernière partie est l'élément le plus haut). Serhiy Storchaka a implémenté ce nouvel opcode et l'a intégré dans CPython de sorte qu'il est disponible dans Python 3.6 depuis la version beta 1 (et donc dans Python 3.6.0 final).

En conséquence, les chaînes littérales formatées seront les suivantes beaucoup plus vite que string.format . Ils sont aussi souvent beaucoup plus plus rapide que l'ancien formatage de Python 3.6, si l'on se contente d'interpoler str o int objets :

>>> 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}' compile maintenant en

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

Le nouveau BUILD_STRING ainsi qu'une optimisation dans FORMAT_VALUE élimine complètement les 5 premières des 6 sources d'inefficacité. Le site __format__ n'est toujours pas slotted, elle nécessite donc une recherche dans le dictionnaire de la classe et son appel est donc nécessairement plus lent que celui de la méthode __str__ mais un appel peut maintenant être complètement évité dans les cas courants de formatage. int o str les instances (pas les sous-classes !) sans spécification de formatage.

0voto

ideasman42 Points 1682

Juste une mise à jour notant que cela semble être résolu dans la version 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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X