web-dev-qa-db-fra.com

Comment se fait-il que la sérialisation json soit tellement plus rapide que la sérialisation yaml en Python?

J'ai du code qui s'appuie fortement sur yaml pour la sérialisation multilingue et en travaillant sur l'accélération de certaines choses, j'ai remarqué que yaml était incroyablement lent par rapport à d'autres méthodes de sérialisation (par exemple, pickle, json).

Donc, ce qui me souffle vraiment, c'est que json est tellement plus rapide que yaml lorsque la sortie est presque identique.

>>> import yaml, cjson; d={'foo': {'bar': 1}}
>>> yaml.dump(d, Dumper=yaml.SafeDumper)
'foo: {bar: 1}\n'
>>> cjson.encode(d)
'{"foo": {"bar": 1}}'
>>> import yaml, cjson;
>>> timeit("yaml.dump(d, Dumper=yaml.SafeDumper)", setup="import yaml; d={'foo': {'bar': 1}}", number=10000)
44.506911039352417
>>> timeit("yaml.dump(d, Dumper=yaml.CSafeDumper)", setup="import yaml; d={'foo': {'bar': 1}}", number=10000)
16.852826118469238
>>> timeit("cjson.encode(d)", setup="import cjson; d={'foo': {'bar': 1}}", number=10000)
0.073784112930297852

CSafeDumper et cjson de PyYaml sont tous les deux écrits en C donc ce n'est pas comme si c'était un problème de vitesse C vs Python. J'ai même ajouté des données aléatoires pour voir si cjson fait de la mise en cache, mais c'est toujours beaucoup plus rapide que PyYaml. Je me rends compte que yaml est un surensemble de json, mais comment le sérialiseur yaml pourrait-il être 2 ordres de grandeur plus lent avec une entrée aussi simple?

58
guidoism

En général, ce n'est pas la complexité de la sortie qui détermine la vitesse d'analyse, mais la complexité de l'entrée acceptée. La grammaire JSON est très concise . Les analyseurs YAML sont relativement complexes , ce qui entraîne une augmentation des frais généraux.

Le principal objectif de conception de JSON est la simplicité et l'universalité. Ainsi, JSON est trivial à générer et à analyser, au prix d'une lisibilité humaine réduite. Il utilise également un modèle d'information de dénominateur commun le plus bas, garantissant que toutes les données JSON peuvent être facilement traitées par tous les environnements de programmation modernes.

En revanche, les principaux objectifs de conception de YAML sont la lisibilité humaine et la prise en charge de la sérialisation de structures de données natives arbitraires. Ainsi, YAML permet des fichiers extrêmement lisibles, mais est plus complexe à générer et à analyser. De plus, YAML s'aventure au-delà des types de données de dénominateur commun les plus bas, nécessitant un traitement plus complexe lors du passage entre différents environnements de programmation.

Je ne suis pas un implémenteur de l'analyseur YAML, donc je ne peux pas parler spécifiquement des ordres de grandeur sans quelques données de profilage et un grand corpus d'exemples. Dans tous les cas, assurez-vous de tester un grand nombre d'entrées avant de vous sentir confiant dans les chiffres de référence.

Mise à jour Oups, mauvaise lecture de la question. : La sérialisation peut toujours être extrêmement rapide malgré la grande grammaire d'entrée; cependant, en parcourant la source, cela ressemble à la sérialisation au niveau Python de PyYAML --- (construit un graphique de représentation tandis que simplejson encode builtin Python types de données directement en morceaux de texte.

58
cdleary

Dans les applications sur lesquelles j'ai travaillé, l'inférence de type entre les chaînes et les nombres (float/int) est celle où la plus grande surcharge est pour analyser yaml car les chaînes peuvent être écrites sans guillemets. Étant donné que toutes les chaînes de json sont entre guillemets, il n'y a pas de retour en arrière lors de l'analyse des chaînes. Un bon exemple où cela ralentirait est la valeur 0000000000000000000s. Vous ne pouvez pas dire que cette valeur est une chaîne avant d'avoir lu jusqu'à la fin.

Les autres réponses sont correctes mais c'est un détail spécifique que j'ai découvert en pratique.

28
twosnac

En parlant d'efficacité, j'ai utilisé YAML pendant un certain temps et je me suis senti attiré par la simplicité que certaines attributions de nom/valeur prennent dans cette langue. Cependant, dans le processus, j'ai trébuché tellement et si souvent sur l'une des finesses de YAML, de subtiles variations dans la grammaire qui vous permettent d'écrire des cas spéciaux dans un style plus concis. En fin de compte, bien que la grammaire de YAML soit presque certainement formellement cohérente, elle m'a laissé un certain sentiment d’imprécision. Je me suis alors limité à ne pas toucher au code YAML existant et fonctionnel et à écrire tout ce qui était nouveau dans une syntaxe plus détournée et à sécurité intégrée, ce qui m'a fait abandonner tout YAML. Le résultat est que YAML essaie de ressembler à une norme W3C et produit une petite bibliothèque de littérature difficile à lire concernant ses concepts et ses règles.

Je pense que cela représente de loin des frais généraux intellectuels plus importants que nécessaire. Regardez SGML/XML: développé par IBM dans les années 60, normalisé par l'ISO, connu (sous une forme abrégée et modifiée) comme HTML pour des millions de personnes, documenté et documenté et documenté à nouveau dans le monde entier. Vient le petit JSON et tue ce dragon. Comment JSON a-t-il pu devenir si largement utilisé en si peu de temps, avec un seul site Web maigre (et un luminaire javascript pour le soutenir)? C'est dans sa simplicité, la pure absence de doute dans sa grammaire, sa facilité d'apprentissage et d'utilisation.

XML et YAML sont difficiles pour les humains et pour les ordinateurs. JSON est assez convivial et facile pour les humains et les ordinateurs.

20
flow

Un aperçu rapide de python-yaml suggère que sa conception est beaucoup plus complexe que celle de cjson:

>>> dir(cjson)
['DecodeError', 'EncodeError', 'Error', '__doc__', '__file__', '__name__', '__package__', 
'__version__', 'decode', 'encode']

>>> dir(yaml)
['AliasEvent', 'AliasToken', 'AnchorToken', 'BaseDumper', 'BaseLoader', 'BlockEndToken',
 'BlockEntryToken', 'BlockMappingStartToken', 'BlockSequenceStartToken', 'CBaseDumper',
'CBaseLoader', 'CDumper', 'CLoader', 'CSafeDumper', 'CSafeLoader', 'CollectionEndEvent', 
'CollectionNode', 'CollectionStartEvent', 'DirectiveToken', 'DocumentEndEvent', 'DocumentEndToken', 
'DocumentStartEvent', 'DocumentStartToken', 'Dumper', 'Event', 'FlowEntryToken', 
'FlowMappingEndToken', 'FlowMappingStartToken', 'FlowSequenceEndToken', 'FlowSequenceStartToken', 
'KeyToken', 'Loader', 'MappingEndEvent', 'MappingNode', 'MappingStartEvent', 'Mark', 
'MarkedYAMLError', 'Node', 'NodeEvent', 'SafeDumper', 'SafeLoader', 'ScalarEvent', 
'ScalarNode', 'ScalarToken', 'SequenceEndEvent', 'SequenceNode', 'SequenceStartEvent', 
'StreamEndEvent', 'StreamEndToken', 'StreamStartEvent', 'StreamStartToken', 'TagToken', 
'Token', 'ValueToken', 'YAMLError', 'YAMLObject', 'YAMLObjectMetaclass', '__builtins__', 
'__doc__', '__file__', '__name__', '__package__', '__path__', '__version__', '__with_libyaml__', 
'add_constructor', 'add_implicit_resolver', 'add_multi_constructor', 'add_multi_representer', 
'add_path_resolver', 'add_representer', 'compose', 'compose_all', 'composer', 'constructor', 
'cyaml', 'dump', 'dump_all', 'dumper', 'emit', 'emitter', 'error', 'events', 'load', 
'load_all', 'loader', 'nodes', 'parse', 'parser', 'reader', 'representer', 'resolver', 
'safe_dump', 'safe_dump_all', 'safe_load', 'safe_load_all', 'scan', 'scanner', 'serialize', 
'serialize_all', 'serializer', 'tokens']

Des conceptions plus complexes signifient presque toujours des conceptions plus lentes, et c'est beaucoup plus complexe que la plupart des gens n'en auront jamais besoin.

12
Glenn Maynard

Bien que vous ayez une réponse acceptée, malheureusement, cela ne fait que faire un geste dans la direction de la documentation PyYAML et cite une déclaration dans cette documentation qui n'est pas correcte: PyYAML ne pas fait un graphique de représentation pendant le dumping, il crée un flux lineair (et tout comme json conserve un ensemble d'ID pour voir s'il y a des récursions).


Tout d'abord, vous devez vous rendre compte que tandis que le dumper cjson est un code C fabriqué à la main uniquement, CSafeDumper de YAML partage deux des quatre étapes de vidage (Representer et Resolver) avec le normal Python SafeDumper et que les deux autres étapes (le sérialiseur et l'émetteur) ne sont pas écrites entièrement à la main en C, mais se composent d'un module Cython qui appelle la bibliothèque C libyaml pour émettre.


En dehors de cette partie importante, la réponse simple à votre question pourquoi cela prend plus de temps, est que le dumping YAML fait plus. Ce n'est pas tant parce que YAML est plus difficile que le prétend @flow, mais parce que ce supplément que YAML peut faire, le rend beaucoup plus puissant que JSON et également plus convivial, si vous devez traiter le résultat avec un éditeur. Cela signifie que plus de temps est passé dans la bibliothèque YAML même lors de l'application de ces fonctionnalités supplémentaires, et dans de nombreux cas, il suffit également de vérifier si quelque chose s'applique.

Voici un exemple: même si vous n'avez jamais parcouru le code PyYAML, vous aurez remarqué que le dumper ne cite pas foo et bar. Ce n'est pas parce que ces chaînes sont des clés, car YAML n'a pas la restriction que JSON a, qu'une clé pour un mappage doit être une chaîne. Par exemple. une chaîne Python qui est une valeur dans le mappage peut peut également être non citée (c'est-à-dire simple).

L'accent est mis sur peut, car il n'en est pas toujours ainsi. Prenez par exemple une chaîne composée uniquement de caractères numériques: 12345678. Cela doit être écrit avec des guillemets, sinon cela ressemblerait exactement à un nombre (et relu en tant que tel lors de l'analyse).

Comment PyYAML sait-il quand citer une chaîne et quand non? Lors du vidage, il vide d'abord la chaîne, puis analyse le résultat pour s'assurer que lorsqu'il lit ce résultat, il obtient la valeur d'origine. Et si cela ne s'avère pas être le cas, il applique des guillemets.

Permettez-moi de répéter la partie importante de la phrase précédente, afin que vous n'ayez pas à la relire:

il vide la chaîne, puis analyse le résultat

Cela signifie qu'il applique toutes les expressions rationnelles correspondantes lors du chargement pour voir si le scalaire résultant se chargerait sous forme d'entier, de flottant, de booléen, de datetime, etc., pour déterminer si des guillemets doivent être appliqués ou non.¹


Dans toute application réelle avec des données complexes, un dumper/chargeur basé sur JSON est trop simple à utiliser directement et beaucoup plus d'intelligence doit être dans votre programme par rapport au dumping des mêmes données complexes directement dans YAML. Un exemple simplifié est lorsque vous souhaitez travailler avec des horodatages, dans ce cas, vous devez convertir une chaîne d'avant en arrière en datetime.datetime vous-même si vous utilisez JSON. Pendant le chargement, vous devez le faire en fonction du fait qu'il s'agit d'une valeur associée à une clé (espérons-le reconnaissable):

{ "datetime": "2018-09-03 12:34:56" }

ou avec une position dans une liste:

["FirstName", "Lastname", "1991-09-12 08:45:00"]

ou basé sur le format de la chaîne (par exemple en utilisant l'expression régulière).

Dans tous ces cas, beaucoup plus de travail doit être fait dans votre programme. Il en va de même pour le dumping et cela ne signifie pas seulement un temps de développement supplémentaire.

Permet de régénérer vos synchronisations avec ce que j'obtiens sur ma machine afin que nous puissions les comparer avec d'autres mesures. J'ai quelque peu réécrit votre code, car il était incomplet (timeit?) Et j'ai importé d'autres choses deux fois. Il était également impossible de simplement couper et coller à cause du >>> instructions.

from __future__ import print_function

import sys
import yaml
import cjson
from timeit import timeit

NR=10000
ds = "; d={'foo': {'bar': 1}}"
d = {'foo': {'bar': 1}}

print('yaml.SafeDumper:', end=' ')
yaml.dump(d, sys.stdout, Dumper=yaml.SafeDumper)
print('cjson.encode:   ', cjson.encode(d))
print()


res = timeit("yaml.dump(d, Dumper=yaml.SafeDumper)", setup="import yaml"+ds, number=NR)
print('yaml.SafeDumper ', res)
res = timeit("yaml.dump(d, Dumper=yaml.CSafeDumper)", setup="import yaml"+ds, number=NR)
print('yaml.CSafeDumper', res)
res = timeit("cjson.encode(d)", setup="import cjson"+ds, number=NR)
print('cjson.encode    ', res)

et cela donne:

yaml.SafeDumper: foo: {bar: 1}
cjson.encode:    {"foo": {"bar": 1}}

yaml.SafeDumper  3.06794905663
yaml.CSafeDumper 0.781533956528
cjson.encode     0.0133550167084

Permet maintenant de vider une structure de données simple qui inclut un datetime

import datetime
from collections import Mapping, Sequence  # python 2.7 has no .abc

d = {'foo': {'bar': datetime.datetime(1991, 9, 12, 8, 45, 0)}}

def stringify(x, key=None):
    # key parameter can be used to dump
    if isinstance(x, str):
       return x
    if isinstance(x, Mapping):
       res = {}
       for k, v in x.items():
           res[stringify(k, key=True)] = stringify(v)  # 
       return res
    if isinstance(x, Sequence):
        res = [stringify(k) for k in x]
        if key:
            res = repr(res)
        return res
    if isinstance(x, datetime.datetime):
        return x.isoformat(sep=' ')
    return repr(x)

print('yaml.CSafeDumper:', end=' ')
yaml.dump(d, sys.stdout, Dumper=yaml.CSafeDumper)
print('cjson.encode:    ', cjson.encode(stringify(d)))
print()

Cela donne:

yaml.CSafeDumper: foo: {bar: '1991-09-12 08:45:00'}
cjson.encode:     {"foo": {"bar": "1991-09-12 08:45:00"}}

Pour le timing de ce qui précède, j'ai créé un module myjson qui enveloppe cjson.encode et a défini stringify ci-dessus. Si vous l'utilisez:

d = {'foo': {'bar': datetime.datetime(1991, 9, 12, 8, 45, 0)}}
ds = 'import datetime, myjson, yaml; d=' + repr(d)
res = timeit("yaml.dump(d, Dumper=yaml.CSafeDumper)", setup=ds, number=NR)
print('yaml.CSafeDumper', res)
res = timeit("myjson.encode(d)", setup=ds, number=NR)
print('cjson.encode    ', res)

donnant:

yaml.CSafeDumper 0.813436031342
cjson.encode     0.151570081711

Cette sortie encore assez simple, vous ramène déjà de deux ordres de grandeur de vitesse à moins d'un seul ordre de grandeur.


Les scalaires simples et la mise en forme de style bloc de YAML améliorent la lisibilité des données. Le fait que vous puissiez avoir une virgule de fin dans une séquence (ou un mappage) réduit les échecs lors de la modification manuelle des données YAML comme avec les mêmes données dans JSON.

Les balises YAML permettent une indication dans les données de vos types (complexes). Lorsque vous utilisez JSON , vous devez faire attention, dans votre code, à quelque chose de plus complexe que les mappages, séquences, entiers, flottants, booléens et chaînes. Un tel code nécessite du temps de développement et il est peu probable qu'il soit aussi rapide que python-cjson (vous êtes bien sûr également libre d'écrire votre code en C).

Le vidage de certaines données, comme les structures de données récursives (par exemple les données topologiques) ou les clés complexes, est prédéfini dans la bibliothèque PyYAML. Là, la bibliothèque JSON ne contient que des erreurs et implémente une solution de contournement qui n'est pas triviale et ralentit probablement les choses, les différences de vitesse étant moins pertinentes.

Une telle puissance et flexibilité ont un prix inférieur à la vitesse. Lorsque vous videz de nombreuses choses simples, JSON est le meilleur choix, il est peu probable que vous modifiez le résultat à la main de toute façon. Pour tout ce qui implique l'édition ou des objets complexes ou les deux, vous devriez toujours envisager d'utiliser YAML.


¹ Il est possible de forcer le vidage de toutes les chaînes Python en tant que scalaires YAML avec des guillemets (doubles), mais définir le style ne suffit pas pour empêcher toute relecture.

2
Anthon